├── .editorconfig ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── __tests__ └── index.js ├── package-lock.json ├── package.json ├── src ├── index.d.ts └── index.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | 3 | name: Tests 4 | 5 | on: [push, pull_request] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [10.x, 12.x, 14.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm ci 22 | - run: npm test 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /npm-debug.log 5 | .DS_Store 6 | .idea 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Colin van Eenige 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prefers 2 | 3 | [![NPM](http://img.shields.io/npm/v/prefers.svg)](https://www.npmjs.com/package/prefers) 4 | [![Tests](https://github.com/vaneenige/prefers/workflows/Tests/badge.svg?branch=master)](https://github.com/vaneenige/prefers/actions?query=workflow%3ATests) 5 | [![gzip size](http://img.badgesize.io/https://unpkg.com/prefers/dist/index.js?compression=gzip)](https://unpkg.com/prefers) 6 | [![TypeScript](https://img.shields.io/static/v1.svg?label=&message=TypeScript&color=294E80)](https://www.typescriptlang.org/) 7 | 8 | Detect system (or manually set) preferences for color scheme and reduced motion. 9 | 10 | Works in all modern browsers and the difference to user experience is day and night! 😬 11 | 12 | 13 | 14 | --- 15 | 16 | 17 | ### Detect color scheme 18 | 19 | ```js 20 | import { prefers, setPrefers } from 'prefers'; 21 | 22 | // When system preference is light 23 | prefers('color-scheme'); // light 24 | prefers('color-scheme', 'light'); // true 25 | 26 | // Manually set preference (in localStorage) 27 | setPrefers('color-scheme', 'dark'); 28 | prefers('color-scheme'); // dark 29 | prefers('color-scheme', 'dark'); // true 30 | 31 | // Remove manually set preference 32 | setPrefers('color-scheme', false); // removed 33 | 34 | // When system has no preference, fallback to default 35 | prefers('color-scheme', 'light', true); // light 36 | prefers('color-scheme', 'dark', true); // dark 37 | ``` 38 | 39 | > Note: Manually set preference will take priority over system preference. 40 | 41 | ### Detect reduced motion 42 | 43 | ```js 44 | import { prefers, setPrefers } from 'prefers'; 45 | 46 | // Assume reduced motion is turned off 47 | prefers('reduced-motion'); // false 48 | setPrefers('reduced-motion', 'reduce'); // turn on 49 | 50 | // Assume reduced motion is turned on 51 | prefers('reduced-motion'); // true 52 | setPrefers('reduced-motion', false); // turn off 53 | ``` 54 | 55 | --- 56 | 57 | ### Tip 58 | 59 | For the best experience it's highly recommended to check for the preference as soon as possible. For example: If you use (p)react, call it before rendering the application. This way, if you're switching CSS variables or a class, the first render will match the preference. 60 | 61 | ### Other preferences? 62 | 63 | There's a draft which describes more preferences we can possibly detect in the future. Once these actually become available, this library will include them! 64 | 65 | Have a look at the [W3C Working Draft for Media Queries Level 5](https://www.w3.org/TR/mediaqueries-5/#media-descriptor-table). 66 | 67 | ### License 68 | 69 | MIT © Colin van Eenige 70 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | import { prefers, setPrefers } from '../src'; 2 | 3 | function mockMatchMedia(property, value) { 4 | global.matchMedia = (media) => { 5 | return { matches: media === `(prefers-${property}: ${value})` }; 6 | }; 7 | } 8 | 9 | const storage = new Map(); 10 | global.localStorage = { 11 | getItem: (key) => storage.get(key) || null, 12 | setItem: (key, value) => storage.set(key, value), 13 | clear: storage.clear(), 14 | }; 15 | 16 | describe('prefers', () => { 17 | beforeEach(() => { 18 | localStorage.clear(); 19 | global.window = true; 20 | }); 21 | 22 | it('should include a prefers function', () => { 23 | expect(typeof prefers).toBe('function'); 24 | }); 25 | 26 | it('should include a setPrefers function', () => { 27 | expect(typeof prefers).toBe('function'); 28 | }); 29 | 30 | it('should not break with server side rendering', () => { 31 | delete global.window; 32 | expect(prefers()).toBe(null); 33 | }); 34 | 35 | describe('color-scheme', () => { 36 | it('should match the light color scheme', () => { 37 | mockMatchMedia('color-scheme', 'light'); 38 | expect(prefers('color-scheme')).toBe('light'); 39 | expect(prefers('color-scheme', 'light')).toBe(true); 40 | }); 41 | it('should match the dark color scheme', () => { 42 | mockMatchMedia('color-scheme', 'dark'); 43 | expect(prefers('color-scheme')).toBe('dark'); 44 | expect(prefers('color-scheme', 'dark')).toBe(true); 45 | }); 46 | it('should match the no-preference color scheme', () => { 47 | mockMatchMedia('color-scheme', 'no-preference'); 48 | expect(prefers('color-scheme')).toBe('no-preference'); 49 | expect(prefers('color-scheme', 'no-preference')).toBe(true); 50 | }); 51 | it("should optionally fallback if there's no preference", () => { 52 | mockMatchMedia('color-scheme', 'no-preference'); 53 | expect(prefers('color-scheme', 'light', true)).toBe(true); 54 | mockMatchMedia('color-scheme', 'dark'); 55 | expect(prefers('color-scheme', 'light', true)).toBe(false); 56 | }); 57 | it('should respect manually set preference', () => { 58 | setPrefers('color-scheme', 'dark'); 59 | mockMatchMedia('color-scheme', 'light'); 60 | expect(prefers('color-scheme')).toBe('dark'); 61 | expect(prefers('color-scheme', 'dark')).toBe(true); 62 | }); 63 | it('should remove preference provided false', () => { 64 | mockMatchMedia('color-scheme', 'light'); 65 | setPrefers('color-scheme', 'dark'); 66 | expect(prefers('color-scheme')).toBe('dark'); 67 | setPrefers('color-scheme', false); 68 | expect(prefers('color-scheme')).toBe('light'); 69 | }); 70 | }); 71 | 72 | describe('reduced-*', () => { 73 | it('should match the reduced motion', () => { 74 | mockMatchMedia('reduced-motion', 'reduce'); 75 | expect(prefers('reduced-motion')).toBe(true); 76 | }); 77 | it('should respect manually set preference', () => { 78 | setPrefers('reduced-motion', 'reduce'); 79 | mockMatchMedia('reduced-motion', 'reduce'); 80 | expect(prefers('reduced-motion')).toBe(true); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prefers", 3 | "version": "1.0.0", 4 | "description": "Detect system (or manually set) preferences for color scheme and reduced motion.", 5 | "main": "dist/index.js", 6 | "umd:main": "dist/index.js", 7 | "module": "dist/index.module.js", 8 | "jsnext:main": "dist/index.module.js", 9 | "directories": { 10 | "test": "test" 11 | }, 12 | "scripts": { 13 | "build": "microbundle src/index.js --no-sourcemap", 14 | "test": "eslint src __tests__ && tsc && jest --coverage", 15 | "prepare": "$npm_execpath run test" 16 | }, 17 | "author": { 18 | "name": "Colin van Eenige", 19 | "email": "cvaneenige@gmail.com" 20 | }, 21 | "license": "MIT", 22 | "typings": "src/index.d.ts", 23 | "babel": { 24 | "presets": [ 25 | "@babel/preset-env" 26 | ] 27 | }, 28 | "eslintConfig": { 29 | "extends": "preact" 30 | }, 31 | "prettier": { 32 | "printWidth": 100, 33 | "singleQuote": true, 34 | "trailingComma": "es5" 35 | }, 36 | "keywords": [ 37 | "prefers-color-scheme", 38 | "prefers-reduced-motion", 39 | "system preference", 40 | "dark mode" 41 | ], 42 | "files": [ 43 | "src", 44 | "dist" 45 | ], 46 | "devDependencies": { 47 | "babel-preset-env": "^1.7.0", 48 | "babel-register": "^6.26.0", 49 | "eslint": "^7.0.0", 50 | "eslint-config-preact": "^1.1.1", 51 | "jest": "^26.0.1", 52 | "json-schema": "^0.2.5", 53 | "microbundle": "^0.12.0", 54 | "node": "^12.16.3", 55 | "typescript": "^3.8.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export function prefers(property: 'color-scheme'): 'light' | 'dark' | 'no-preference'; 2 | 3 | export function prefers( 4 | property: 'color-scheme', 5 | value: 'light' | 'dark' | 'no-preference', 6 | fallback?: boolean 7 | ): boolean; 8 | 9 | export function setPrefers( 10 | property: 'color-scheme', 11 | value: 'light' | 'dark' | 'no-preference' | false 12 | ): void; 13 | 14 | export function prefers(property: 'reduced-motion'): boolean; 15 | 16 | export function setPrefers( 17 | property: 'reduced-motion', 18 | value: 'reduce' | 'no-preference' | false 19 | ): void; 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | 'color-scheme': ['light', 'dark', 'no-preference'], 3 | 'reduced-*': ['reduce', 'no-preference'], 4 | }; 5 | 6 | function match(p, v) { 7 | return matchMedia(`(prefers-${p}: ${v})`).matches; 8 | } 9 | 10 | function setPrefers(property, value) { 11 | localStorage[value === false ? 'removeItem' : 'setItem'](`prefers-${property}`, value); 12 | } 13 | 14 | function prefers(property, value, fallback) { 15 | if (typeof window === 'undefined') return null; 16 | const reduced = property.indexOf('reduced') !== -1; 17 | // See if a preference is manually set 18 | const preference = localStorage.getItem(`prefers-${property}`); 19 | // If preference is set, return wether it matches 20 | if (preference !== null) { 21 | if (reduced) return true; 22 | if (value === undefined) return preference; 23 | return preference === value; 24 | } 25 | // Check all options to see which one matches 26 | const p = options[reduced ? 'reduced-*' : property].find((option) => match(property, option)); 27 | // If reduced return boolean 28 | if (reduced) return p === 'reduce'; 29 | // If value is empty, just return the match 30 | if (value === undefined) return p; 31 | // If match equals the value return ture 32 | if (p === value) return true; 33 | // If it should fallback to the value and no preference is set, return true 34 | return fallback && p === 'no-preference'; 35 | } 36 | 37 | export { prefers, setPrefers }; 38 | 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "strict": true, 5 | }, 6 | "files": ["src/index.d.ts"] 7 | } 8 | --------------------------------------------------------------------------------