├── .tape.js ├── .gitignore ├── .editorconfig ├── .github └── workflows │ └── test.yml ├── test ├── basic.css ├── basic.expect.css └── basic.result.css ├── src ├── postcss.js └── browser.js ├── CHANGELOG.md ├── .rollup.js ├── CONTRIBUTING.md ├── package.json ├── cli.js ├── README-POSTCSS.md ├── README-BROWSER.md ├── README.md ├── INSTALL.md └── LICENSE.md /.tape.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'basic': { 3 | message: 'supports basic usage' 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /browser.* 3 | /index.* 4 | /postcss.* 5 | package-lock.json 6 | yarn.lock 7 | *.log* 8 | .* 9 | !.editorconfig 10 | !.gitignore 11 | !.rollup.js 12 | !.tape.js 13 | !.travis.yml 14 | !.github 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [{*.{json,md,yml},.*}] 14 | indent_size = 2 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node: [12, 16] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: ${{ matrix.node }} 16 | 17 | - run: yarn install --ignore-scripts 18 | - run: yarn run test 19 | -------------------------------------------------------------------------------- /test/basic.css: -------------------------------------------------------------------------------- 1 | @media (prefers-color-scheme: no-preference) { 2 | :root { 3 | --site-bgcolor: #f9f9f9; 4 | --site-color: #111; 5 | } 6 | } 7 | 8 | @media (prefers-color-scheme: dark) { 9 | :root { 10 | --site-bgcolor: #1b1b1b; 11 | --site-color: #fff; 12 | } 13 | } 14 | 15 | @media (prefers-color-scheme: light) { 16 | :root { 17 | --site-bgcolor: #fff; 18 | --site-color: #222; 19 | } 20 | } 21 | 22 | body { 23 | background-color: var(--site-bgcolor, #f9f9f9); 24 | color: var(--site-color, #111); 25 | font: 100%/1.5 system-ui; 26 | } 27 | -------------------------------------------------------------------------------- /test/basic.expect.css: -------------------------------------------------------------------------------- 1 | @media (color-index: 22) { 2 | :root { 3 | --site-bgcolor: #f9f9f9; 4 | --site-color: #111; 5 | } 6 | } 7 | 8 | @media (prefers-color-scheme: no-preference) { 9 | :root { 10 | --site-bgcolor: #f9f9f9; 11 | --site-color: #111; 12 | } 13 | } 14 | 15 | @media (color-index: 48) { 16 | :root { 17 | --site-bgcolor: #1b1b1b; 18 | --site-color: #fff; 19 | } 20 | } 21 | 22 | @media (prefers-color-scheme: dark) { 23 | :root { 24 | --site-bgcolor: #1b1b1b; 25 | --site-color: #fff; 26 | } 27 | } 28 | 29 | @media (color-index: 70) { 30 | :root { 31 | --site-bgcolor: #fff; 32 | --site-color: #222; 33 | } 34 | } 35 | 36 | @media (prefers-color-scheme: light) { 37 | :root { 38 | --site-bgcolor: #fff; 39 | --site-color: #222; 40 | } 41 | } 42 | 43 | body { 44 | background-color: var(--site-bgcolor, #f9f9f9); 45 | color: var(--site-color, #111); 46 | font: 100%/1.5 system-ui; 47 | } 48 | -------------------------------------------------------------------------------- /test/basic.result.css: -------------------------------------------------------------------------------- 1 | @media (color-index: 22) { 2 | :root { 3 | --site-bgcolor: #f9f9f9; 4 | --site-color: #111; 5 | } 6 | } 7 | 8 | @media (prefers-color-scheme: no-preference) { 9 | :root { 10 | --site-bgcolor: #f9f9f9; 11 | --site-color: #111; 12 | } 13 | } 14 | 15 | @media (color-index: 48) { 16 | :root { 17 | --site-bgcolor: #1b1b1b; 18 | --site-color: #fff; 19 | } 20 | } 21 | 22 | @media (prefers-color-scheme: dark) { 23 | :root { 24 | --site-bgcolor: #1b1b1b; 25 | --site-color: #fff; 26 | } 27 | } 28 | 29 | @media (color-index: 70) { 30 | :root { 31 | --site-bgcolor: #fff; 32 | --site-color: #222; 33 | } 34 | } 35 | 36 | @media (prefers-color-scheme: light) { 37 | :root { 38 | --site-bgcolor: #fff; 39 | --site-color: #222; 40 | } 41 | } 42 | 43 | body { 44 | background-color: var(--site-bgcolor, #f9f9f9); 45 | color: var(--site-color, #111); 46 | font: 100%/1.5 system-ui; 47 | } 48 | -------------------------------------------------------------------------------- /src/postcss.js: -------------------------------------------------------------------------------- 1 | const mediaRegExp = /^media$/i; 2 | const prefersInterfaceRegExp = /\(\s*prefers-color-scheme\s*:\s*(dark|light|no-preference)\s*\)/i; 3 | const colorIndexByStyle = { dark: 48, light: 70, 'no-preference': 22 }; 4 | const prefersInterfaceReplacer = ($0, style) => `(color-index: ${colorIndexByStyle[style.toLowerCase()]})`; 5 | 6 | const creator = opts => { 7 | const preserve = 'preserve' in Object(opts) ? opts.preserve : true; 8 | 9 | return { 10 | postcssPlugin: 'postcss-prefers-color-scheme', 11 | Once: root => { 12 | root.walkAtRules(mediaRegExp, atRule => { 13 | const { params } = atRule; 14 | const altParams = params.replace(prefersInterfaceRegExp, prefersInterfaceReplacer); 15 | 16 | if (params !== altParams) { 17 | if (preserve) { 18 | atRule.cloneBefore({ params: altParams }); 19 | } else { 20 | atRule.params = altParams; 21 | } 22 | } 23 | }); 24 | }, 25 | }; 26 | }; 27 | 28 | creator.postcss = true; 29 | 30 | export default creator; 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes to Prefers Color Scheme 2 | 3 | ### 5.0.0 (September 17, 2021) 4 | 5 | - Updated: Support for PostCS 8+ (major). 6 | - Updated: Support for Node 12+ (major). 7 | 8 | ### 4.0.0 (May 24, 2019) 9 | 10 | - Updated: `postcss` to 7.0.16 (patch) 11 | - Updated: Node 8+ compatibility (major) 12 | 13 | ### 3.1.1 (November 10, 2018) 14 | 15 | - Updated: Project organization. No functional changes. 16 | 17 | ### 3.1.0 (November 10, 2018) 18 | 19 | - Include CLI tool for transforming CSS without any installation 20 | - Update documentation 21 | 22 | ### 3.0.0 (November 4, 2018) 23 | 24 | - Preserve `prefers-color-scheme` queries by default for non-JS environments 25 | - Remove `prefers-color-scheme` queries on the frontend for JS environments 26 | 27 | ### 2.0.0 (November 3, 2018) 28 | 29 | - The client library now returns an object with various features, including: 30 | - `scheme` to get or set the preferred color scheme 31 | - `hasNativeSupport` to report whether `prefers-color-scheme` is supported 32 | - `onChange` to listen for when the preferred color scheme changes 33 | - `removeListener` to destroy the native `prefers-color-scheme` listener 34 | 35 | ### 1.0.0 (September 24, 2018) 36 | 37 | - Initial version 38 | -------------------------------------------------------------------------------- /.rollup.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | 4 | const isBrowser = String(process.env.NODE_ENV).includes('browser'); 5 | const isBrowserMin = String(process.env.NODE_ENV).includes('browser:min'); 6 | const isPostCSS = String(process.env.NODE_ENV).includes('postcss'); 7 | 8 | // support IE9+ browsers, otherwise node 6+ 9 | const targets = isBrowser ? 'ie >= 9' : { node: 10 }; 10 | 11 | // read from src/browser.js for browsers/node, src/postcss.js for postcss 12 | const input = isPostCSS ? 'src/postcss.js' : 'src/browser.js'; 13 | 14 | // write to browser.js/browser.min.js for browsers, index.js/index.mjs for node 15 | const output = isPostCSS 16 | ? [ 17 | { file: 'postcss.js', format: 'cjs', exports: 'default', sourcemap: true, strict: false }, 18 | { file: 'postcss.mjs', format: 'esm', sourcemap: true, strict: false } 19 | ] : isBrowser 20 | ? { file: `browser${isBrowserMin ? '.min' : ''}.js`, format: 'iife', name: 'initPrefersColorScheme', sourcemap: !isBrowserMin } 21 | : [ 22 | { file: 'index.js', format: 'cjs', exports: 'default', sourcemap: true }, 23 | { file: 'index.mjs', format: 'esm', sourcemap: true } 24 | ]; 25 | 26 | // use babel, and also terser to minify browser.min.js 27 | const plugins = [ 28 | babel({ 29 | babelHelpers: 'bundled', 30 | presets: [ 31 | ['@babel/env', { modules: false, targets }] 32 | ] 33 | }) 34 | ].concat( 35 | isBrowserMin 36 | ? terser({ 37 | mangle: true 38 | }) 39 | : [] 40 | ); 41 | 42 | export default { input, output, plugins }; 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Prefers Color Scheme 2 | 3 | You want to help? You rock! Now, take a moment to be sure your contributions 4 | make sense to everyone else. 5 | 6 | ## Reporting Issues 7 | 8 | Found a problem? Want a new feature? 9 | 10 | - See if your issue or idea has [already been reported]. 11 | - Provide a [reduced test case] or a [live example]. 12 | 13 | Remember, a bug is a _demonstrable problem_ caused by _our_ code. 14 | 15 | ## Submitting Pull Requests 16 | 17 | Pull requests are the greatest contributions, so be sure they are focused in 18 | scope and avoid unrelated commits. 19 | 20 | 1. To begin; [fork this project], clone your fork, and add our upstream. 21 | ```bash 22 | # Clone your fork of the repo into the current directory 23 | git clone git@github.com:YOUR_USER/css-prefers-color-scheme.git 24 | 25 | # Navigate to the newly cloned directory 26 | cd css-prefers-color-scheme 27 | 28 | # Assign the original repo to a remote called "upstream" 29 | git remote add upstream git@github.com:csstools/css-prefers-color-scheme.git 30 | 31 | # Install the tools necessary for testing 32 | npm install 33 | ``` 34 | 35 | 2. Create a branch for your feature or fix: 36 | ```bash 37 | # Move into a new branch for your feature 38 | git checkout -b feature/thing 39 | ``` 40 | ```bash 41 | # Move into a new branch for your fix 42 | git checkout -b fix/something 43 | ``` 44 | 45 | 3. If your code follows our practices, then push your feature branch: 46 | ```bash 47 | # Test current code 48 | npm test 49 | ``` 50 | ```bash 51 | # Push the branch for your new feature 52 | git push origin feature/thing 53 | ``` 54 | ```bash 55 | # Or, push the branch for your update 56 | git push origin update/something 57 | ``` 58 | 59 | That’s it! Now [open a pull request] with a clear title and description. 60 | 61 | [already been reported]: https://github.com/csstools/css-prefers-color-scheme/issues 62 | [fork this project]: https://github.com/csstools/css-prefers-color-scheme/fork 63 | [live example]: https://codepen.io/pen 64 | [open a pull request]: https://help.github.com/articles/using-pull-requests/ 65 | [reduced test case]: https://css-tricks.com/reduced-test-cases/ 66 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | const colorIndexRegExp = /((?:not )?all and )?(\(color-index: *(22|48|70)\))/i; 2 | const prefersColorSchemeRegExp = /prefers-color-scheme:/i; 3 | 4 | const prefersColorSchemeInit = initialColorScheme => { 5 | const mediaQueryString = '(prefers-color-scheme: dark)'; 6 | const mediaQueryList = window.matchMedia && matchMedia(mediaQueryString); 7 | const hasNativeSupport = mediaQueryList && mediaQueryList.media === mediaQueryString; 8 | const mediaQueryListener = () => { 9 | set(mediaQueryList.matches ? 'dark' : 'light'); 10 | }; 11 | const removeListener = () => { 12 | if (mediaQueryList) { 13 | mediaQueryList.removeListener(mediaQueryListener); 14 | } 15 | }; 16 | const set = colorScheme => { 17 | if (colorScheme !== currentColorScheme) { 18 | currentColorScheme = colorScheme; 19 | 20 | if (typeof result.onChange === 'function') { 21 | result.onChange(); 22 | } 23 | } 24 | 25 | [].forEach.call(document.styleSheets || [], styleSheet => { 26 | [].forEach.call(styleSheet.cssRules || [], cssRule => { 27 | const colorSchemeMatch = prefersColorSchemeRegExp.test(Object(cssRule.media).mediaText); 28 | 29 | if (colorSchemeMatch) { 30 | const index = [].indexOf.call(cssRule.parentStyleSheet.cssRules, cssRule); 31 | 32 | cssRule.parentStyleSheet.deleteRule(index); 33 | } else { 34 | const colorIndexMatch = (Object(cssRule.media).mediaText || '').match(colorIndexRegExp); 35 | 36 | if (colorIndexMatch) { 37 | cssRule.media.mediaText = ( 38 | (/^dark$/i.test(colorScheme) 39 | ? colorIndexMatch[3] === '48' 40 | : /^light$/i.test(colorScheme) 41 | ? colorIndexMatch[3] === '70' 42 | : colorIndexMatch[3] === '22') 43 | ? 'not all and ' 44 | : '' 45 | ) + cssRule.media.mediaText.replace(colorIndexRegExp, '$2'); 46 | } 47 | } 48 | }); 49 | }); 50 | }; 51 | const result = Object.defineProperty( 52 | { hasNativeSupport, removeListener }, 53 | 'scheme', 54 | { get: () => currentColorScheme, set } 55 | ); 56 | 57 | // initialize the color scheme using the provided value, the system value, or light 58 | let currentColorScheme = initialColorScheme || (mediaQueryList && mediaQueryList.matches ? 'dark' : 'light'); 59 | 60 | set(currentColorScheme); 61 | 62 | // listen for system changes 63 | if (mediaQueryList) { 64 | mediaQueryList.addListener(mediaQueryListener); 65 | } 66 | 67 | return result; 68 | }; 69 | 70 | export default prefersColorSchemeInit; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-prefers-color-scheme", 3 | "version": "5.0.0", 4 | "description": "Use light and dark color schemes in all browsers", 5 | "author": "Jonathan Neal ", 6 | "license": "CC0-1.0", 7 | "repository": "csstools/css-prefers-color-scheme", 8 | "homepage": "https://github.com/csstools/css-prefers-color-scheme#readme", 9 | "bugs": "https://github.com/csstools/css-prefers-color-scheme/issues", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "bin": { 13 | "css-prefers-color-scheme": "cli.js" 14 | }, 15 | "files": [ 16 | "browser.js", 17 | "browser.js.map", 18 | "browser.min.js", 19 | "cli.js", 20 | "index.mjs", 21 | "index.mjs.map", 22 | "index.js", 23 | "index.js.map", 24 | "postcss.js", 25 | "postcss.mjs" 26 | ], 27 | "scripts": { 28 | "build": "npm run build:browser && npm run build:node && npm run build:postcss", 29 | "build:browser": "npm run build:browser:dist && npm run build:browser:min", 30 | "build:browser:dist": "cross-env NODE_ENV=browser rollup --config .rollup.js --silent", 31 | "build:browser:min": "cross-env NODE_ENV=browser:min rollup --config .rollup.js --silent", 32 | "build:node": "rollup --config .rollup.js --silent", 33 | "build:postcss": "cross-env NODE_ENV=postcss rollup --config .rollup.js --silent", 34 | "prepublishOnly": "npm test", 35 | "pretest": "npm run build", 36 | "test": "npm run test:js && npm run test:tape", 37 | "test:js": "eslint src/{*,**/*}.js --cache --ignore-path .gitignore --quiet", 38 | "test:tape": "postcss-tape --plugin postcss.js" 39 | }, 40 | "engines": { 41 | "node": ">=12" 42 | }, 43 | "peerDependencies": { 44 | "postcss": "^8.3" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "7.15.5", 48 | "@babel/preset-env": "7.15.6", 49 | "@rollup/plugin-babel": "5.3.0", 50 | "cross-env": "7.0.3", 51 | "eslint": "7.32.0", 52 | "postcss": "8.3.6", 53 | "postcss-tape": "6.0.1", 54 | "pre-commit": "1.2.2", 55 | "rollup": "2.56.3", 56 | "rollup-plugin-terser": "7.0.2" 57 | }, 58 | "eslintConfig": { 59 | "env": { 60 | "browser": true, 61 | "es6": true, 62 | "node": true 63 | }, 64 | "extends": "eslint:recommended", 65 | "parserOptions": { 66 | "ecmaVersion": 2020, 67 | "sourceType": "module" 68 | }, 69 | "root": true 70 | }, 71 | "keywords": [ 72 | "postcss", 73 | "css", 74 | "postcss-plugin", 75 | "media", 76 | "query", 77 | "prefers", 78 | "color", 79 | "scheme", 80 | "dark", 81 | "light", 82 | "no-preference", 83 | "mode", 84 | "queries", 85 | "interface" 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const prefersColorScheme = require('./postcss'); 5 | 6 | if (process.argv.length < 3) { 7 | console.log([ 8 | 'Prefers Color Scheme\n', 9 | ' Transforms CSS with @media (prefers-color-scheme) {}\n', 10 | 'Usage:\n', 11 | ' css-prefers-color-scheme source.css transformed.css', 12 | ' css-prefers-color-scheme --in=source.css --out=transformed.css --opts={}', 13 | ' echo "@media (prefers-color-scheme: dark) {}" | css-prefers-color-scheme\n' 14 | ].join('\n')); 15 | process.exit(0); 16 | } 17 | 18 | // get process and plugin options from the command line 19 | const fileRegExp = /^[\w\/.]+$/; 20 | const argRegExp = /^--(\w+)=("|')?(.+)\2$/; 21 | const relaxedJsonRegExp = /(['"])?([a-z0-9A-Z_]+)(['"])?:/g; 22 | const argo = process.argv.slice(2).reduce( 23 | (object, arg) => { 24 | const argMatch = arg.match(argRegExp); 25 | const fileMatch = arg.match(fileRegExp); 26 | 27 | if (argMatch) { 28 | object[argMatch[1]] = argMatch[3]; 29 | } else if (fileMatch) { 30 | if (object.from === '') { 31 | object.from = arg; 32 | } else if (object.to === '') { 33 | object.to = arg; 34 | } 35 | } 36 | 37 | return object; 38 | }, 39 | { from: '', to: '', opts: 'null' } 40 | ); 41 | 42 | // get css from command line arguments or stdin 43 | (argo.from === '' ? getStdin() : readFile(argo.from)) 44 | .then(css => { 45 | const pluginOpts = JSON.parse(argo.opts.replace(relaxedJsonRegExp, '"$2": ')); 46 | const processOptions = Object.assign({ from: argo.from, to: argo.to || argo.from }, argo.map ? { map: JSON.parse(argo.map) } : {}); 47 | 48 | const result = prefersColorScheme.process(css, processOptions, pluginOpts); 49 | 50 | if (argo.to === '') { 51 | return result.css; 52 | } else { 53 | return writeFile(argo.to, result.css).then( 54 | () => `CSS was written to "${argo.to}"` 55 | ) 56 | } 57 | }).then( 58 | result => { 59 | console.log(result); 60 | 61 | process.exit(0); 62 | }, 63 | error => { 64 | console.error(error); 65 | 66 | process.exit(1); 67 | } 68 | ); 69 | 70 | function readFile (pathname) { 71 | return new Promise((resolve, reject) => { 72 | fs.readFile(pathname, 'utf8', (error, data) => { 73 | if (error) { 74 | reject(error); 75 | } else { 76 | resolve(data); 77 | } 78 | }); 79 | }); 80 | } 81 | 82 | function writeFile (pathname, data) { 83 | return new Promise((resolve, reject) => { 84 | fs.writeFile(pathname, data, (error, content) => { 85 | if (error) { 86 | reject(error); 87 | } else { 88 | resolve(content); 89 | } 90 | }); 91 | }); 92 | } 93 | 94 | function getStdin () { 95 | return new Promise(resolve => { 96 | let data = ''; 97 | 98 | if (process.stdin.isTTY) { 99 | resolve(data); 100 | } else { 101 | process.stdin.setEncoding('utf8'); 102 | 103 | process.stdin.on('readable', () => { 104 | let chunk; 105 | 106 | while (chunk = process.stdin.read()) { 107 | data += chunk; 108 | } 109 | }); 110 | 111 | process.stdin.on('end', () => { 112 | resolve(data); 113 | }); 114 | } 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /README-POSTCSS.md: -------------------------------------------------------------------------------- 1 | # Prefers Color Scheme [][Prefers Color Scheme] 2 | 3 | [![NPM Version][npm-img]][npm-url] 4 | [![Build Status][cli-img]][cli-url] 5 | [![Support Chat][git-img]][git-url] 6 | 7 | [Prefers Color Scheme] transforms `prefers-color-scheme` media queries into 8 | something all browsers understand. 9 | 10 | ```css 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --site-bgcolor: #1b1b1b; 14 | --site-color: #fff; 15 | } 16 | } 17 | 18 | body { 19 | background-color: var(--site-bgcolor, #f9f9f9); 20 | color: var(--site-color, #111); 21 | font: 100%/1.5 system-ui; 22 | } 23 | 24 | /* becomes */ 25 | 26 | @media (color-index: 48) { 27 | :root { 28 | --site-bgcolor: #1b1b1b; 29 | --site-color: #fff; 30 | } 31 | } 32 | 33 | @media (prefers-color-scheme: dark) { 34 | :root { 35 | --site-bgcolor: #1b1b1b; 36 | --site-color: #fff; 37 | } 38 | } 39 | 40 | body { 41 | background-color: var(--site-bgcolor, #f9f9f9); 42 | color: var(--site-color, #111); 43 | font: 100%/1.5 system-ui; 44 | } 45 | ``` 46 | 47 | ## Usage 48 | 49 | Use [Prefers Color Scheme] to process your CSS: 50 | 51 | ```bash 52 | npx css-prefers-color-scheme INPUT.css OUTPUT.css 53 | ``` 54 | 55 | Or use it within Node: 56 | 57 | ```js 58 | const postcssPrefersColorScheme = require('css-prefers-color-scheme/postcss'); 59 | 60 | postcssPrefersColorScheme.process(YOUR_CSS /*, processOptions, pluginOptions */); 61 | ``` 62 | 63 | Or use it as a [PostCSS] plugin: 64 | 65 | ```js 66 | const postcss = require('postcss'); 67 | const postcssPrefersColorScheme = require('css-prefers-color-scheme/postcss'); 68 | 69 | postcss([ 70 | postcssPrefersColorScheme(/* pluginOptions */) 71 | ]).process(YOUR_CSS /*, processOptions */); 72 | ``` 73 | 74 | [Prefers Color Scheme] runs in all Node environments, with special 75 | instructions for: 76 | 77 | | [Node](INSTALL.md#node) | [Webpack](INSTALL.md#webpack) | [Create React App](INSTALL.md#create-react-app) | [Gulp](INSTALL.md#gulp) | [Grunt](INSTALL.md#grunt) | 78 | | --- | --- | --- | --- | --- | 79 | 80 | ### Options 81 | 82 | #### preserve 83 | 84 | The `preserve` option determines whether the original `prefers-color-scheme` 85 | query will be preserved or removed. By default, it is preserved. 86 | 87 | ```js 88 | require('css-prefers-color-scheme/postcss')({ preserve: false }); 89 | ``` 90 | 91 | ```css 92 | @media (prefers-color-scheme: dark) { 93 | body { 94 | background-color: black; 95 | } 96 | } 97 | 98 | /* becomes */ 99 | 100 | @media (color-index: 48) { 101 | body { 102 | background-color: black; 103 | } 104 | } 105 | ``` 106 | 107 | [cli-img]: https://img.shields.io/travis/csstools/css-prefers-color-scheme/master.svg 108 | [cli-url]: https://travis-ci.org/csstools/css-prefers-color-scheme 109 | [git-img]: https://img.shields.io/badge/support-chat-blue.svg 110 | [git-url]: https://gitter.im/postcss/postcss 111 | [npm-img]: https://img.shields.io/npm/v/css-prefers-color-scheme.svg 112 | [npm-url]: https://www.npmjs.com/package/css-prefers-color-scheme 113 | 114 | [PostCSS]: https://github.com/postcss/postcss 115 | [Prefers Color Scheme]: https://github.com/csstools/css-prefers-color-scheme 116 | -------------------------------------------------------------------------------- /README-BROWSER.md: -------------------------------------------------------------------------------- 1 | # Prefers Color Scheme [][Prefers Color Scheme] 2 | 3 | [![NPM Version][npm-img]][npm-url] 4 | [![Build Status][cli-img]][cli-url] 5 | [![Support Chat][git-img]][git-url] 6 | 7 | [Prefers Color Scheme] applies color schemes with fallbacks provided by the 8 | [Prefers Color Scheme PostCSS plugin](README-POSTCSS.md). 9 | 10 | ```js 11 | // initialize prefersColorScheme (applies the current OS color scheme, if available) 12 | const prefersColorScheme = require('css-prefers-color-scheme')(); 13 | 14 | // apply "dark" queries (you can also apply "light") 15 | prefersColorScheme.scheme = 'dark'; 16 | ``` 17 | 18 | [Prefers Color Scheme] works in all major browsers, including Safari 6+ and 19 | Internet Explorer 9+ without any polyfills. 20 | [See it for yourself.](https://app.crossbrowsertesting.com/public/i76b092cd2b52b86/screenshots/z25c0ccdfcc9c9b8956f?size=medium&type=windowed) 21 | 22 | To maintain compatibility with browsers supporting `prefers-color-scheme`, the 23 | library will remove `prefers-color-scheme` media queries in favor of 24 | cross-browser compatible `color-index` media queries. This ensures a seemless 25 | experience, even when JavaScript is unable to run. 26 | 27 | ## Usage 28 | 29 | Use [Prefers Color Scheme] to activate your `prefers-color-scheme` queries: 30 | 31 | ```js 32 | const prefersColorScheme = require('css-prefers-color-scheme')(); 33 | ``` 34 | 35 | By default, the current OS color scheme is applied if your browser supports it. 36 | Otherwise, the light color scheme is applied. You may override this by passing 37 | in a color scheme. 38 | 39 | ```js 40 | const prefersColorScheme = require('css-prefers-color-scheme')('dark'); 41 | ``` 42 | 43 | The `prefersColorScheme` object returns the following properties — `value`, 44 | `hasNativeSupport`, `onChange`, and `removeListener`. 45 | 46 | ### value 47 | 48 | The `value` property returns the currently preferred color scheme, and it can 49 | be changed. 50 | 51 | ```js 52 | const prefersColorScheme = require('css-prefers-color-scheme')(); 53 | 54 | // log the preferred color scheme 55 | console.log(prefersColorScheme.scheme); 56 | 57 | // apply "dark" queries 58 | prefersColorScheme.scheme = 'dark'; 59 | ``` 60 | 61 | ### hasNativeSupport 62 | 63 | The `hasNativeSupport` boolean represents whether `prefers-color-scheme` is 64 | supported by the browser. 65 | 66 | ### onChange 67 | 68 | The optional `onChange` function is run when the preferred color scheme is 69 | changed, either from the OS or manually. 70 | 71 | ### removeListener 72 | 73 | The `removeListener` function removes the native `prefers-color-scheme` 74 | listener, which may or may not be applied, depending on your browser support. 75 | This is provided to give you complete control over plugin cleanup. 76 | 77 | [cli-img]: https://img.shields.io/travis/csstools/css-prefers-color-scheme/master.svg 78 | [cli-url]: https://travis-ci.org/csstools/css-prefers-color-scheme 79 | [git-img]: https://img.shields.io/badge/support-chat-blue.svg 80 | [git-url]: https://gitter.im/postcss/postcss 81 | [npm-img]: https://img.shields.io/npm/v/css-prefers-color-scheme.svg 82 | [npm-url]: https://www.npmjs.com/package/css-prefers-color-scheme 83 | 84 | [PostCSS]: https://github.com/postcss/postcss 85 | [Prefers Color Scheme]: https://github.com/csstools/css-prefers-color-scheme 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
⚠️ Prefers Color Scheme was moved to @csstools/postcss-plugins. ⚠️
2 | Read the announcement
3 | 4 | # Prefers Color Scheme [][Prefers Color Scheme] 5 | 6 | [![NPM Version][npm-img]][npm-url] 7 | [![Build Status][cli-img]][cli-url] 8 | [![Support Chat][git-img]][git-url] 9 | 10 | [Prefers Color Scheme] lets you use light and dark color schemes in all 11 | browsers, following the [Media Queries] specification. 12 | 13 | [!['Can I use' table](https://caniuse.bitsofco.de/image/prefers-color-scheme.png)](https://caniuse.com/#feat=prefers-color-scheme) 14 | 15 | ## Usage 16 | 17 | From the command line, transform CSS files that use `prefers-color-scheme` 18 | media queries: 19 | 20 | ```bash 21 | npx css-prefers-color-scheme SOURCE.css TRANSFORMED.css 22 | ``` 23 | 24 | Next, use that transformed CSS with this script: 25 | 26 | ```html 27 | 28 | 29 | 32 | ``` 33 | 34 | Dependencies got you down? Don’t worry, this script is only 537 bytes. 35 | 36 | ## Usage 37 | 38 | - First, transform `prefers-color-scheme` queries using this 39 | [PostCSS plugin](README-POSTCSS.md). 40 | - Next, apply light and dark color schemes everywhere using this 41 | [browser script](README-BROWSER.md). 42 | 43 | --- 44 | 45 | ## How does it work? 46 | 47 | [Prefers Color Scheme] uses a [PostCSS plugin](README-POSTCSS.md) to transform 48 | `prefers-color-scheme` queries into `color-index` queries. This changes 49 | `prefers-color-scheme: dark` into `(color-index: 48)`, 50 | `prefers-color-scheme: light` into `(color-index: 70)`, and 51 | `prefers-color-scheme: no-preference` into `(color-index: 22)`. 52 | 53 | The frontend receives these `color-index` queries, which are understood in all 54 | major browsers going back to Internet Explorer 9. However, since browsers only 55 | apply `color-index` queries of `0`, our color scheme values are ignored. 56 | 57 | [Prefers Color Scheme] uses a [browser script](README-BROWSER.md) to change 58 | `(color-index: 48)` queries into `not all and (color-index: 48)` in order to 59 | activate “dark mode” specific CSS, and it changes `(color-index: 70)` queries 60 | into `not all and (color-index: 48)` to activate “light mode” specific CSS. 61 | 62 | ```css 63 | @media (color-index: 70) { /* prefers-color-scheme: light */ 64 | body { 65 | background-color: white; 66 | color: black; 67 | } 68 | } 69 | ``` 70 | 71 | Since these media queries are accessible to `document.styleSheet`, no CSS 72 | parsing is required. 73 | 74 | ## Why does the fallback work this way? 75 | 76 | The value of `48` is chosen for dark mode because it is the keycode for `0`, 77 | the hexidecimal value of black. Likewise, `70` is chosen for light mode because 78 | it is the keycode for `f`, the hexidecimal value of white. 79 | 80 | [cli-img]: https://github.com/csstools/css-prefers-color-scheme/workflows/test/badge.svg 81 | [cli-url]: https://github.com/csstools/css-prefers-color-scheme/actions/workflows/test.yml?query=workflow/test 82 | [git-img]: https://img.shields.io/badge/support-chat-blue.svg 83 | [git-url]: https://gitter.im/postcss/postcss 84 | [npm-img]: https://img.shields.io/npm/v/css-prefers-color-scheme.svg 85 | [npm-url]: https://www.npmjs.com/package/css-prefers-color-scheme 86 | 87 | [PostCSS]: https://github.com/postcss/postcss 88 | [Prefers Color Scheme]: https://github.com/csstools/css-prefers-color-scheme 89 | [Media Queries]: https://drafts.csswg.org/mediaqueries-5/#descdef-media-prefers-color-scheme 90 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installing Prefers Color Scheme 2 | 3 | [Prefers Color Scheme] runs in all Node environments, with special 4 | instructions for: 5 | 6 | | [Node](#node) | [Webpack](#webpack) | [Create React App](#create-react-app) | [Gulp](#gulp) | [Grunt](#grunt) | 7 | | --- | --- | --- | --- | --- | 8 | 9 | ## Node 10 | 11 | Add [Prefers Color Scheme] to your project: 12 | 13 | ```bash 14 | npm install css-prefers-color-scheme 15 | ``` 16 | 17 | Use [Prefers Color Scheme] to process your CSS: 18 | 19 | ```js 20 | const postcssPrefers = require('css-prefers-color-scheme/postcss'); 21 | 22 | prefersColorScheme.process(YOUR_CSS /*, processOptions, pluginOptions */); 23 | ``` 24 | 25 | Or use it as a [PostCSS] plugin: 26 | 27 | ```js 28 | const postcss = require('postcss'); 29 | const postcssPrefers = require('css-prefers-color-scheme/postcss'); 30 | 31 | postcss([ 32 | prefersColorScheme(/* pluginOptions */) 33 | ]).process(YOUR_CSS /*, processOptions */); 34 | ``` 35 | 36 | ## Webpack 37 | 38 | Add [PostCSS Loader] to your project: 39 | 40 | ```bash 41 | npm install postcss-loader --save-dev 42 | ``` 43 | 44 | Use [Prefers Color Scheme] in your Webpack configuration: 45 | 46 | ```js 47 | const postcssPrefers = require('css-prefers-color-scheme/postcss'); 48 | 49 | module.exports = { 50 | module: { 51 | rules: [ 52 | { 53 | test: /\.css$/, 54 | use: [ 55 | 'style-loader', 56 | { loader: 'css-loader', options: { importLoaders: 1 } }, 57 | { loader: 'postcss-loader', options: { 58 | ident: 'postcss', 59 | plugins: () => [ 60 | prefersColorScheme(/* pluginOptions */) 61 | ] 62 | } } 63 | ] 64 | } 65 | ] 66 | } 67 | } 68 | ``` 69 | 70 | ## Create React App 71 | 72 | Add [React App Rewired] and [React App Rewire PostCSS] to your project: 73 | 74 | ```bash 75 | npm install react-app-rewired react-app-rewire-postcss --save-dev 76 | ``` 77 | 78 | Use [React App Rewire PostCSS] and [Prefers Color Scheme] in your 79 | `config-overrides.js` file: 80 | 81 | ```js 82 | const reactAppRewirePostcss = require('react-app-rewire-postcss'); 83 | const postcssPrefers = require('css-prefers-color-scheme/postcss'); 84 | 85 | export default config => reactAppRewirePostcss(config, { 86 | plugins: () => [ 87 | prefersColorScheme(/* pluginOptions */) 88 | ] 89 | }); 90 | ``` 91 | 92 | ## Gulp 93 | 94 | Add [Gulp PostCSS] to your project: 95 | 96 | ```bash 97 | npm install gulp-postcss --save-dev 98 | ``` 99 | 100 | Use [Prefers Color Scheme] in your Gulpfile: 101 | 102 | ```js 103 | const postcss = require('gulp-postcss'); 104 | const postcssPrefers = require('css-prefers-color-scheme/postcss'); 105 | 106 | gulp.task('css', () => gulp.src('./src/*.css').pipe( 107 | postcss([ 108 | prefersColorScheme(/* pluginOptions */) 109 | ]) 110 | ).pipe( 111 | gulp.dest('.') 112 | )); 113 | ``` 114 | 115 | ## Grunt 116 | 117 | Add [Grunt PostCSS] to your project: 118 | 119 | ```bash 120 | npm install grunt-postcss --save-dev 121 | ``` 122 | 123 | Use [Prefers Color Scheme] in your Gruntfile: 124 | 125 | ```js 126 | const postcssPrefers = require('css-prefers-color-scheme/postcss'); 127 | 128 | grunt.loadNpmTasks('grunt-postcss'); 129 | 130 | grunt.initConfig({ 131 | postcss: { 132 | options: { 133 | use: [ 134 | prefersColorScheme(/* pluginOptions */) 135 | ] 136 | }, 137 | dist: { 138 | src: '*.css' 139 | } 140 | } 141 | }); 142 | ``` 143 | 144 | [Gulp PostCSS]: https://github.com/postcss/gulp-postcss 145 | [Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss 146 | [PostCSS]: https://github.com/postcss/postcss 147 | [PostCSS Loader]: https://github.com/postcss/postcss-loader 148 | [Prefers Color Scheme]: https://github.com/jonathantneal/postcss-prefers-color-scheme 149 | [React App Rewire PostCSS]: https://github.com/csstools/react-app-rewire-postcss 150 | [React App Rewired]: https://github.com/timarney/react-app-rewired 151 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # CC0 1.0 Universal 2 | 3 | ## Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an “owner”) of an original work of 8 | authorship and/or a database (each, a “Work”). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific works 12 | (“Commons”) that the public can reliably and without fear of later claims of 13 | infringement build upon, modify, incorporate in other works, reuse and 14 | redistribute as freely as possible in any form whatsoever and for any purposes, 15 | including without limitation commercial purposes. These owners may contribute 16 | to the Commons to promote the ideal of a free culture and the further 17 | production of creative, cultural and scientific works, or to gain reputation or 18 | greater distribution for their Work in part through the use and efforts of 19 | others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation of 22 | additional consideration or compensation, the person associating CC0 with a 23 | Work (the “Affirmer”), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and 25 | publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights (“Copyright and 31 | Related Rights”). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 1. the right to reproduce, adapt, distribute, perform, display, communicate, 34 | and translate a Work; 35 | 2. moral rights retained by the original author(s) and/or performer(s); 36 | 3. publicity and privacy rights pertaining to a person’s image or likeness 37 | depicted in a Work; 38 | 4. rights protecting against unfair competition in regards to a Work, 39 | subject to the limitations in paragraph 4(i), below; 40 | 5. rights protecting the extraction, dissemination, use and reuse of data in 41 | a Work; 42 | 6. database rights (such as those arising under Directive 96/9/EC of the 43 | European Parliament and of the Council of 11 March 1996 on the legal 44 | protection of databases, and under any national implementation thereof, 45 | including any amended or successor version of such directive); and 46 | 7. other similar, equivalent or corresponding rights throughout the world 47 | based on applicable law or treaty, and any national implementations 48 | thereof. 49 | 50 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 51 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 52 | unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright 53 | and Related Rights and associated claims and causes of action, whether now 54 | known or unknown (including existing as well as future claims and causes of 55 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 56 | duration provided by applicable law or treaty (including future time 57 | extensions), (iii) in any current or future medium and for any number of 58 | copies, and (iv) for any purpose whatsoever, including without limitation 59 | commercial, advertising or promotional purposes (the “Waiver”). Affirmer 60 | makes the Waiver for the benefit of each member of the public at large and 61 | to the detriment of Affirmer’s heirs and successors, fully intending that 62 | such Waiver shall not be subject to revocation, rescission, cancellation, 63 | termination, or any other legal or equitable action to disrupt the quiet 64 | enjoyment of the Work by the public as contemplated by Affirmer’s express 65 | Statement of Purpose. 66 | 67 | 3. Public License Fallback. Should any part of the Waiver for any reason be 68 | judged legally invalid or ineffective under applicable law, then the Waiver 69 | shall be preserved to the maximum extent permitted taking into account 70 | Affirmer’s express Statement of Purpose. In addition, to the extent the 71 | Waiver is so judged Affirmer hereby grants to each affected person a 72 | royalty-free, non transferable, non sublicensable, non exclusive, 73 | irrevocable and unconditional license to exercise Affirmer’s Copyright and 74 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 75 | maximum duration provided by applicable law or treaty (including future time 76 | extensions), (iii) in any current or future medium and for any number of 77 | copies, and (iv) for any purpose whatsoever, including without limitation 78 | commercial, advertising or promotional purposes (the “License”). The License 79 | shall be deemed effective as of the date CC0 was applied by Affirmer to the 80 | Work. Should any part of the License for any reason be judged legally 81 | invalid or ineffective under applicable law, such partial invalidity or 82 | ineffectiveness shall not invalidate the remainder of the License, and in 83 | such case Affirmer hereby affirms that he or she will not (i) exercise any 84 | of his or her remaining Copyright and Related Rights in the Work or (ii) 85 | assert any associated claims and causes of action with respect to the Work, 86 | in either case contrary to Affirmer’s express Statement of Purpose. 87 | 88 | 4. Limitations and Disclaimers. 89 | 1. No trademark or patent rights held by Affirmer are waived, abandoned, 90 | surrendered, licensed or otherwise affected by this document. 91 | 2. Affirmer offers the Work as-is and makes no representations or warranties 92 | of any kind concerning the Work, express, implied, statutory or 93 | otherwise, including without limitation warranties of title, 94 | merchantability, fitness for a particular purpose, non infringement, or 95 | the absence of latent or other defects, accuracy, or the present or 96 | absence of errors, whether or not discoverable, all to the greatest 97 | extent permissible under applicable law. 98 | 3. Affirmer disclaims responsibility for clearing rights of other persons 99 | that may apply to the Work or any use thereof, including without 100 | limitation any person’s Copyright and Related Rights in the Work. 101 | Further, Affirmer disclaims responsibility for obtaining any necessary 102 | consents, permissions or other rights required for any use of the Work. 103 | 4. Affirmer understands and acknowledges that Creative Commons is not a 104 | party to this document and has no duty or obligation with respect to this 105 | CC0 or use of the Work. 106 | 107 | For more information, please see 108 | http://creativecommons.org/publicdomain/zero/1.0/. 109 | --------------------------------------------------------------------------------