├── .gitignore ├── demo ├── cat.jpg ├── sun.png ├── moon.png ├── system.png ├── unchecked.svg ├── checked.svg ├── moon.svg ├── sun.svg ├── light.css ├── with-flashing.html ├── dark.css ├── internal-stylesheets.html ├── without-flashing.html ├── dark-mode-toggle-playground.mjs ├── system.svg ├── unstyled.html ├── slider.css ├── dist.html ├── common.css └── index.html ├── CODE_OF_CONDUCT.md ├── .github └── workflows │ └── main.yml ├── eslint.config.js ├── .prettierrc ├── CONTRIBUTING.md ├── src ├── dark-mode-toggle.d.ts ├── dark-mode-toggle-stylesheets-loader.js ├── template-contents.tpl └── dark-mode-toggle.mjs ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /demo/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/dark-mode-toggle/main/demo/cat.jpg -------------------------------------------------------------------------------- /demo/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/dark-mode-toggle/main/demo/sun.png -------------------------------------------------------------------------------- /demo/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/dark-mode-toggle/main/demo/moon.png -------------------------------------------------------------------------------- /demo/system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/dark-mode-toggle/main/demo/system.png -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All Google open source projects are covered by our 4 | [community guidelines](https://opensource.google/conduct/) which define the kind 5 | of respectful behavior we expect of all participants. 6 | -------------------------------------------------------------------------------- /demo/unchecked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/checked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | moon 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2-beta 11 | with: 12 | fetch-depth: 1 13 | - uses: preactjs/compressed-size-action@v1 14 | with: 15 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // eslint.config.js 2 | module.exports = [ 3 | { 4 | languageOptions: { 5 | ecmaVersion: 8, 6 | sourceType: 'module', 7 | }, 8 | rules: { 9 | 'require-jsdoc': 'off', 10 | 'max-len': [ 11 | 'error', 12 | { 13 | ignoreTemplateLiterals: true, 14 | }, 15 | ], 16 | }, 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "embeddedLanguageFormatting": "auto", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxSingleQuote": false, 8 | "printWidth": 80, 9 | "proseWrap": "always", 10 | "quoteProps": "consistent", 11 | "requirePragma": false, 12 | "semi": true, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "all", 16 | "useTabs": false, 17 | "vueIndentScriptAndStyle": false 18 | } 19 | -------------------------------------------------------------------------------- /demo/sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/light.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | :root { 18 | color-scheme: light; /* stylelint-disable-line property-no-unknown */ 19 | 20 | --background-color: rgb(240 240 240); 21 | --text-color: rgb(15 15 15); 22 | --shadow-color: rgb(15 15 15 / 50%); 23 | --accent-color: rgb(240 0 0 / 50%); 24 | } 25 | -------------------------------------------------------------------------------- /demo/with-flashing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 14 | 15 | 16 |

Demo with flashing

17 | 18 |
Refresh to test: Refresh
19 | 20 |
21 | Go to the version without flashing 22 |
23 | 24 |
25 | Switch theme: 26 | 27 |
28 | 29 | 30 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/dark.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | :root { 18 | color-scheme: dark; /* stylelint-disable-line property-no-unknown */ 19 | 20 | --background-color: rgb(15 15 15); 21 | --text-color: rgb(240 240 240); 22 | --shadow-color: rgb(240 240 240 / 50%); 23 | --accent-color: rgb(0 0 240 / 50%); 24 | } 25 | 26 | img { 27 | filter: grayscale(50%); 28 | } 29 | 30 | .icon { 31 | filter: invert(100%); 32 | } 33 | 34 | a { 35 | color: yellow; 36 | } 37 | -------------------------------------------------------------------------------- /demo/internal-stylesheets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 22 | 23 | 24 | 25 |

Demo with internal stylesheets

26 |

27 | Lorem ipsum dolor sit amet, legere ancillae ne vis. Ne vim laudem accusam 28 | consectetuer, eu utinam integre abhorreant sea. Quo eius veri ei, nibh 29 | invenire democritum vel in, utamur vulputate id est. Possit ceteros vis 30 | an. 31 |

32 |
33 | Switch theme: 34 | 35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /demo/without-flashing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 |

Demo without flashing

21 |
Refresh to test: Refresh
22 |
Go to the version with flashing
23 |
24 | Switch theme: 25 | 26 |
27 | 28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows 28 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /demo/dark-mode-toggle-playground.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | ((doc) => { 18 | const themeColor = doc.querySelector('meta[name="theme-color"]'); 19 | const icon = doc.querySelector('link[rel="icon"]'); 20 | const colorScheme = doc.querySelector('meta[name="color-scheme"]'); 21 | const body = doc.body; 22 | 23 | doc.addEventListener('colorschemechange', (e) => { 24 | // The event fires right before the color scheme goes into effect, 25 | // so we need the `color` value. 26 | themeColor.content = getComputedStyle(body).color; 27 | colorScheme.content = e.detail.colorScheme; 28 | icon.href = e.detail.colorScheme === 'dark' ? 'moon.png' : 'sun.png'; 29 | console.log( 30 | `${e.target.id} changed the color scheme to ${e.detail.colorScheme}`, 31 | ); 32 | }); 33 | 34 | doc.addEventListener('permanentcolorscheme', (e) => { 35 | const permanent = e.detail.permanent; 36 | console.log( 37 | `${permanent ? 'R' : 'Not r'}emembering the last selected mode.`, 38 | ); 39 | }); 40 | })(document); 41 | -------------------------------------------------------------------------------- /demo/system.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | moon 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/unstyled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | 17 | 18 |

Unstyled out-of-the-box experience

19 | 47 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/dark-mode-toggle.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Reflects the user’s desire that the page use a light or dark color theme. 3 | */ 4 | export type ColorScheme = 'light' | 'dark'; 5 | 6 | export class DarkModeToggle extends HTMLElement { 7 | /** 8 | * The user's preferred color scheme. 9 | */ 10 | mode: ColorScheme; 11 | 12 | /** 13 | * The "switch" appearance conveys the idea of a theme switcher (light/dark), 14 | * whereas "toggle" conveys the idea of a dark mode toggle (on/off). 15 | * The "three-way" option will feature a central state that aligns with the user's system preferred color mode, while both the left and right states will consistently apply a fixed color scheme, irrespective of the system settings. 16 | */ 17 | appearance: 'toggle' | 'switch' | 'three-way'; 18 | 19 | /** 20 | * If true, remember the last selected mode ("dark" or "light"), 21 | * which allows the user to permanently override their usual preferred color scheme. 22 | */ 23 | permanent: boolean; 24 | 25 | /** 26 | * Any string value that represents the legend for the toggle or switch. 27 | */ 28 | legend: string; 29 | 30 | /** 31 | * Any string value that represents the label for the "light" mode. 32 | */ 33 | light: string; 34 | 35 | /** 36 | * Any string value that represents the label for the "dark" mode. 37 | */ 38 | dark: string; 39 | 40 | /** 41 | * Any string value that represents the label for the 42 | * "remember the last selected mode" functionality. 43 | */ 44 | remember: string; 45 | } 46 | 47 | /** 48 | * Fired when the color scheme gets changed. 49 | */ 50 | export type ColorSchemeChangeEvent = CustomEvent<{ colorScheme: ColorScheme }>; 51 | 52 | /** 53 | * Fired when the color scheme should be permanently remembered or not. 54 | */ 55 | export type PermanentColorSchemeEvent = CustomEvent<{ permanent: boolean }>; 56 | 57 | declare global { 58 | interface HTMLElementTagNameMap { 59 | 'dark-mode-toggle': DarkModeToggle; 60 | } 61 | 62 | interface GlobalEventHandlersEventMap { 63 | colorschemechange: ColorSchemeChangeEvent; 64 | permanentcolorscheme: PermanentColorSchemeEvent; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/dark-mode-toggle-stylesheets-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @license © 2024 Google LLC. Licensed under the Apache License, Version 2.0. 18 | /** 19 | * Copyright 2024 Google LLC 20 | * 21 | * Licensed under the Apache License, Version 2.0 (the "License"); 22 | * you may not use this file except in compliance with the License. 23 | * You may obtain a copy of the License at 24 | * 25 | * https://www.apache.org/licenses/LICENSE-2.0 26 | * 27 | * Unless required by applicable law or agreed to in writing, software 28 | * distributed under the License is distributed on an "AS IS" BASIS, 29 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 | * See the License for the specific language governing permissions and 31 | * limitations under the License. 32 | */ 33 | 34 | // @license © 2024 Google LLC. Licensed under the Apache License, Version 2.0. 35 | (() => { 36 | const ELEMENT_ID = 'dark-mode-toggle-stylesheets'; 37 | const STORAGE_NAME = 'dark-mode-toggle'; 38 | const LIGHT = 'light'; 39 | const DARK = 'dark'; 40 | 41 | let stylesheetsAndMetatag = document.getElementById(ELEMENT_ID).textContent; 42 | 43 | let mode = null; 44 | try { 45 | mode = localStorage.getItem(STORAGE_NAME); 46 | } catch (e) { 47 | return; 48 | } 49 | 50 | const lightCSSMediaRegex = /\(\s*prefers-color-scheme\s*:\s*light\s*\)/gi; 51 | const darkCSSMediaRegex = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/gi; 52 | const darkLightRegex = /\b(?:dark\s+light|light\s+dark)\b/; 53 | 54 | switch (mode) { 55 | case LIGHT: 56 | stylesheetsAndMetatag = stylesheetsAndMetatag 57 | .replace(lightCSSMediaRegex, '$&, all') 58 | .replace(darkCSSMediaRegex, '$& and not all') 59 | .replace(darkLightRegex, mode); 60 | break; 61 | 62 | case DARK: 63 | stylesheetsAndMetatag = stylesheetsAndMetatag 64 | .replace(darkCSSMediaRegex, '$&, all') 65 | .replace(lightCSSMediaRegex, '$& and not all') 66 | .replace(darkLightRegex, mode); 67 | break; 68 | } 69 | 70 | document.write(stylesheetsAndMetatag); 71 | })(); 72 | -------------------------------------------------------------------------------- /demo/slider.css: -------------------------------------------------------------------------------- 1 | dark-mode-toggle.slider::part(toggleLabel) { 2 | display: inline-block; 3 | position: relative; 4 | height: calc(var(--dark-mode-toggle-icon-size, 1rem) * 2); 5 | width: calc(var(--dark-mode-toggle-icon-size, 1rem) * 3.5); 6 | background-color: #b7bbbd; 7 | border-radius: var(--dark-mode-toggle-icon-size, 1rem); 8 | transition: 0.4s; 9 | } 10 | 11 | dark-mode-toggle.slider[mode='dark']::part(toggleLabel) { 12 | background-color: #4e5255; 13 | } 14 | 15 | dark-mode-toggle.slider::part(toggleLabel)::before { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | position: absolute; 20 | top: calc(var(--dark-mode-toggle-icon-size, 1rem) * 0.25); 21 | left: calc(var(--dark-mode-toggle-icon-size, 1rem) * 0.25); 22 | height: calc(var(--dark-mode-toggle-icon-size, 1rem) * 1.5); 23 | width: calc(var(--dark-mode-toggle-icon-size, 1rem) * 1.5); 24 | border-radius: 100%; 25 | box-shadow: 26 | 0 0.15em 0.3em rgb(0 0 0 / 15%), 27 | 0 0.2em 0.5em rgb(0 0 0 / 30%); 28 | background-color: #fff; 29 | color: #333; 30 | transition: 0.4s; 31 | content: ''; 32 | background-position: center; 33 | background-size: var(--dark-mode-toggle-icon-size, 1rem); 34 | background-image: var(--dark-mode-toggle-light-icon, url('sun.svg')); 35 | box-sizing: border-box; 36 | } 37 | 38 | dark-mode-toggle.slider[mode='dark']::part(toggleLabel)::before { 39 | left: calc(100% - var(--dark-mode-toggle-icon-size, 1rem) * 1.75); 40 | border-color: #000; 41 | background-color: #ccc; 42 | color: #000; 43 | background-size: var(--dark-mode-toggle-icon-size, 1rem); 44 | background-image: var(--dark-mode-toggle-dark-icon, url('moon.svg')); 45 | filter: var(--dark-mode-toggle-icon-filter, invert(100%)); 46 | box-shadow: 0 0.5px hsl(0deg 0% 100% / 16%); 47 | } 48 | 49 | dark-mode-toggle.slider::part(toggleLabel)::after { 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | position: absolute; 54 | top: calc(var(--dark-mode-toggle-icon-size, 1rem) * 0.25); 55 | left: calc(100% - var(--dark-mode-toggle-icon-size, 1rem) * 1.75); 56 | height: calc(var(--dark-mode-toggle-icon-size, 1rem) * 1.5); 57 | width: calc(var(--dark-mode-toggle-icon-size, 1rem) * 1.5); 58 | border-radius: 100%; 59 | color: #333; 60 | content: ''; 61 | background-position: center; 62 | background-size: var(--dark-mode-toggle-icon-size, 1rem); 63 | background-image: var(--dark-mode-toggle-dark-icon, url('moon.svg')); 64 | background-repeat: no-repeat; 65 | box-sizing: border-box; 66 | opacity: 0.5; 67 | } 68 | 69 | dark-mode-toggle.slider[mode='dark']::part(toggleLabel)::after { 70 | left: calc(var(--dark-mode-toggle-icon-size, 1rem) * 0.25); 71 | background-image: var(--dark-mode-toggle-light-icon, url('sun.svg')); 72 | filter: var(--dark-mode-toggle-icon-filter, invert(100%)); 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dark-mode-toggle", 3 | "version": "0.18.0", 4 | "description": "Web Component that toggles dark mode 🌒", 5 | "main": "./dist/dark-mode-toggle.min.mjs", 6 | "module": "./dist/dark-mode-toggle.min.mjs", 7 | "unpkg": "./dist/dark-mode-toggle.min.mjs", 8 | "exports": { 9 | ".": { 10 | "types": "./src/dark-mode-toggle.d.ts", 11 | "module": "./dist/dark-mode-toggle.min.mjs", 12 | "import": "./dist/dark-mode-toggle.min.mjs", 13 | "browser": "./dist/dark-mode-toggle.min.mjs" 14 | } 15 | }, 16 | "files": [ 17 | "src", 18 | "dist" 19 | ], 20 | "scripts": { 21 | "start": "npx http-server && echo \"Server running on http://localhost:8080/demo/\"", 22 | "clean": "shx rm -rf ./dist && mkdir dist", 23 | "lint:js": "npx eslint \"./src/*.mjs\" --fix && npx eslint \"./demo/*.mjs\" --fix", 24 | "lint:css": "shx cp ./src/template-contents.tpl ./src/template-contents.html && npx stylelint \"./src/*.html\" --fix && shx cp ./src/template-contents.html ./src/template-contents.tpl && shx rm ./src/template-contents.html && npx stylelint \"./demo/*.css\" --fix", 25 | "lint": "npm run lint:js && npm run lint:css", 26 | "fix": "npx prettier --write .", 27 | "build": "npm run clean && npx terser ./src/dark-mode-toggle.mjs --toplevel --mangle-props regex=\\\"^_\\\" --comments /@license/ --ecma=8 -o ./dist/dark-mode-toggle.min.mjs && npx terser ./src/dark-mode-toggle-stylesheets-loader.js --toplevel --mangle-props regex=\\\"^_\\\" --comments /@license/ --ecma=8 -o ./dist/dark-mode-toggle-stylesheets-loader.min.js", 28 | "prepare": "npm run lint && npm run fix && npm run build" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/googlechromelabs/dark-mode-toggle.git" 33 | }, 34 | "keywords": [ 35 | "dark", 36 | "mode" 37 | ], 38 | "author": "Thomas Steiner (https://blog.tomayac.com/)", 39 | "license": "Apache-2.0", 40 | "bugs": { 41 | "url": "https://github.com/googlechromelabs/dark-mode-toggle/issues" 42 | }, 43 | "homepage": "https://github.com/googlechromelabs/dark-mode-toggle#readme", 44 | "devDependencies": { 45 | "eslint": "^9.39.1", 46 | "eslint-config-google": "^0.14.0", 47 | "http-server": "^14.1.1", 48 | "postcss-html": "^1.8.0", 49 | "prettier": "^3.7.4", 50 | "shx": "^0.4.0", 51 | "stylelint": "^16.26.1", 52 | "stylelint-config-standard": "^39.0.1", 53 | "terser": "^5.44.1" 54 | }, 55 | "stylelint": { 56 | "extends": "stylelint-config-standard", 57 | "rules": { 58 | "selector-type-no-unknown": [ 59 | true, 60 | { 61 | "ignore": [ 62 | "custom-elements" 63 | ] 64 | } 65 | ], 66 | "property-no-unknown": [ 67 | true, 68 | { 69 | "ignoreProperties": [] 70 | } 71 | ] 72 | }, 73 | "overrides": [ 74 | { 75 | "files": [ 76 | "*.html", 77 | "**/*.html" 78 | ], 79 | "customSyntax": "postcss-html" 80 | } 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /demo/dist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello Dark Mode 5 | 6 | 7 | 8 | 9 | 10 | 11 | 22 | 27 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |

Hi there!

39 | Sitting cat in front of a tree 45 |

46 | Lorem ipsum dolor sit amet, legere ancillae ne vis. Ne vim laudem 47 | accusam consectetuer, eu utinam integre abhorreant sea. Quo eius veri 48 | ei, nibh invenire democritum vel in, utamur vulputate id est. Possit 49 | ceteros vis an. 50 |

51 |
52 |
53 | Lorem ipsum 54 |
55 | 59 |
60 |
61 | 62 |
63 |
64 | 68 |
69 |
70 | 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 | 82 |
83 |
84 |
85 |

86 | Also see the unstyled variant 87 | that shows the out-of-the-box experience. 88 |

89 |
90 | 93 | 96 | 105 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /demo/common.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2019 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | :root { 18 | --heading-color: red; 19 | --duration: 0.5s; 20 | --timing: ease; 21 | } 22 | 23 | *, 24 | ::before, 25 | ::after { 26 | box-sizing: border-box; 27 | } 28 | 29 | body { 30 | margin: 0; 31 | transition: 32 | color var(--duration) var(--timing), 33 | background-color var(--duration) var(--timing); 34 | font-family: sans-serif; 35 | font-size: 12pt; 36 | background-color: var(--background-color); 37 | color: var(--text-color); 38 | display: flex; 39 | justify-content: center; 40 | } 41 | 42 | main { 43 | margin: 1rem; 44 | max-width: 30rem; 45 | position: relative; 46 | } 47 | 48 | h1 { 49 | color: var(--heading-color); 50 | text-shadow: 0.1rem 0.1rem 0.1rem var(--shadow-color); 51 | transition: text-shadow var(--duration) var(--timing); 52 | } 53 | 54 | img { 55 | max-width: 100%; 56 | height: auto; 57 | transition: filter var(--duration) var(--timing); 58 | } 59 | 60 | p { 61 | line-height: 1.5; 62 | overflow-wrap: break-word; 63 | hyphens: auto; 64 | } 65 | 66 | fieldset { 67 | border: solid 0.1rem; 68 | box-shadow: 0.1rem 0.1rem 0.1rem var(--shadow-color); 69 | transition: box-shadow var(--duration) var(--timing); 70 | } 71 | 72 | div { 73 | padding: 0.5rem; 74 | } 75 | 76 | aside { 77 | position: absolute; 78 | right: 0; 79 | padding: 0.5rem; 80 | } 81 | 82 | aside:nth-of-type(1) { 83 | top: 0; 84 | } 85 | 86 | aside:nth-of-type(2) { 87 | top: 3rem; 88 | } 89 | 90 | aside:nth-of-type(3) { 91 | top: 7rem; 92 | } 93 | 94 | aside:nth-of-type(4) { 95 | top: 12rem; 96 | } 97 | 98 | aside:nth-of-type(5) { 99 | width: 10rem; 100 | top: 16rem; 101 | } 102 | 103 | aside:nth-of-type(6) { 104 | width: 20rem; 105 | top: 0; 106 | left: 0 !important; 107 | } 108 | 109 | #content select, 110 | #content button, 111 | #content input[type='text'], 112 | #content input[type='search'] { 113 | width: 15rem; 114 | } 115 | 116 | dark-mode-toggle { 117 | --dark-mode-toggle-remember-icon-checked: url('checked.svg'); 118 | --dark-mode-toggle-remember-icon-unchecked: url('unchecked.svg'); 119 | --dark-mode-toggle-remember-font: 0.75rem 'Helvetica'; 120 | --dark-mode-toggle-legend-font: bold 0.85rem 'Helvetica'; 121 | --dark-mode-toggle-label-font: 0.85rem 'Helvetica'; 122 | --dark-mode-toggle-color: var(--text-color); 123 | --dark-mode-toggle-background-color: none; 124 | 125 | margin-bottom: 1.5rem; 126 | } 127 | 128 | #dark-mode-toggle-1 { 129 | --dark-mode-toggle-dark-icon: url('sun.png'); 130 | --dark-mode-toggle-light-icon: url('moon.png'); 131 | } 132 | 133 | #dark-mode-toggle-2 { 134 | --dark-mode-toggle-dark-icon: url('sun.svg'); 135 | --dark-mode-toggle-light-icon: url('moon.svg'); 136 | --dark-mode-toggle-icon-size: 2rem; 137 | --dark-mode-toggle-icon-filter: invert(100%); 138 | } 139 | 140 | #dark-mode-toggle-3, 141 | #dark-mode-toggle-4 { 142 | --dark-mode-toggle-dark-icon: url('moon.png'); 143 | --dark-mode-toggle-light-icon: url('sun.png'); 144 | } 145 | 146 | #dark-mode-toggle-3 { 147 | --dark-mode-toggle-remember-filter: invert(100%); 148 | } 149 | 150 | #dark-mode-toggle-4 { 151 | --dark-mode-toggle-active-mode-background-color: var(--accent-color); 152 | --dark-mode-toggle-remember-filter: invert(100%); 153 | } 154 | 155 | #dark-mode-toggle-6 { 156 | --dark-mode-toggle-active-mode-background-color: var(--accent-color); 157 | --dark-mode-toggle-remember-filter: invert(100%); 158 | } 159 | -------------------------------------------------------------------------------- /src/template-contents.tpl: -------------------------------------------------------------------------------- 1 | 166 |
167 |
168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 187 |
188 |
189 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello Dark Mode 5 | 6 | 7 | 8 | 9 | 10 | 11 | 22 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |

Hi there!

40 | Sitting cat in front of a tree 46 |

47 | Lorem ipsum dolor sit amet, legere ancillae ne vis. Ne vim laudem 48 | accusam consectetuer, eu utinam integre abhorreant sea. Quo eius veri 49 | ei, nibh invenire democritum vel in, utamur vulputate id est. Possit 50 | ceteros vis an. 51 |

52 |
53 |
54 | Lorem ipsum 55 |
56 | 60 |
61 |
62 | 63 |
64 |
65 | 69 |
70 |
71 | 75 |
76 |
77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 |
86 |

87 | Also see the unstyled variant 88 | that shows the out-of-the-box experience. 89 |

90 |

91 | Run npm run build and see the 92 | built variant that uses the minified version of 93 | the script in the dist folder. 94 |

95 |

96 | Per default, the custom element is optimized for loading speed. 97 | Depending on your page, this can introduce flashing (example with flashing). At the expense of loading speed using 101 | document.write() and following the 102 | instructions in the README, you can prevent this (example without flashing). 108 |

109 |

110 | You can also check out the 111 | demo with internal stylesheets. 112 |

113 |
114 | 117 | 120 | 129 | 139 | 159 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/dark-mode-toggle.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @license © 2019 Google LLC. Licensed under the Apache License, Version 2.0. 18 | const doc = document; 19 | let store = {}; 20 | try { 21 | store = localStorage; 22 | } catch (err) { 23 | // Do nothing. The user probably blocks cookies. 24 | } 25 | const PREFERS_COLOR_SCHEME = 'prefers-color-scheme'; 26 | const MEDIA = 'media'; 27 | const LIGHT = 'light'; 28 | const DARK = 'dark'; 29 | const SYSTEM = 'system'; 30 | const MQ_DARK = `(${PREFERS_COLOR_SCHEME}:${DARK})`; 31 | const MQ_LIGHT = `(${PREFERS_COLOR_SCHEME}:${LIGHT})`; 32 | const LINK_REL_STYLESHEET = 'link[rel=stylesheet]'; 33 | const STYLE = 'style'; 34 | const REMEMBER = 'remember'; 35 | const LEGEND = 'legend'; 36 | const TOGGLE = 'toggle'; 37 | const SWITCH = 'switch'; 38 | const THREE_WAY = 'three-way'; 39 | const APPEARANCE = 'appearance'; 40 | const PERMANENT = 'permanent'; 41 | const MODE = 'mode'; 42 | const COLOR_SCHEME_CHANGE = 'colorschemechange'; 43 | const PERMANENT_COLOR_SCHEME = 'permanentcolorscheme'; 44 | const ALL = 'all'; 45 | const NOT_ALL = 'not all'; 46 | const NAME = 'dark-mode-toggle'; 47 | const DEFAULT_URL = 'https://googlechromelabs.github.io/dark-mode-toggle/demo/'; 48 | 49 | // See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html ↵ 50 | // #reflecting-content-attributes-in-idl-attributes. 51 | const installStringReflection = (obj, attrName, propName = attrName) => { 52 | Object.defineProperty(obj, propName, { 53 | enumerable: true, 54 | get() { 55 | const value = this.getAttribute(attrName); 56 | return value === null ? '' : value; 57 | }, 58 | set(v) { 59 | this.setAttribute(attrName, v); 60 | }, 61 | }); 62 | }; 63 | 64 | const installBoolReflection = (obj, attrName, propName = attrName) => { 65 | Object.defineProperty(obj, propName, { 66 | enumerable: true, 67 | get() { 68 | return this.hasAttribute(attrName); 69 | }, 70 | set(v) { 71 | if (v) { 72 | this.setAttribute(attrName, ''); 73 | } else { 74 | this.removeAttribute(attrName); 75 | } 76 | }, 77 | }); 78 | }; 79 | 80 | const template = doc.createElement('template'); 81 | // ⚠️ Note: this is a minified version of `src/template-contents.tpl`. 82 | // Compress the CSS with https://cssminifier.com/, then paste it here. 83 | 84 | template.innerHTML = `
`; 85 | 86 | export class DarkModeToggle extends HTMLElement { 87 | static get observedAttributes() { 88 | return [MODE, APPEARANCE, PERMANENT, LEGEND, LIGHT, DARK, REMEMBER]; 89 | } 90 | 91 | constructor() { 92 | super(); 93 | 94 | installStringReflection(this, MODE); 95 | installStringReflection(this, APPEARANCE); 96 | installStringReflection(this, LEGEND); 97 | installStringReflection(this, LIGHT); 98 | installStringReflection(this, DARK); 99 | installStringReflection(this, SYSTEM); 100 | installStringReflection(this, REMEMBER); 101 | 102 | installBoolReflection(this, PERMANENT); 103 | 104 | this._darkCSS = null; 105 | this._lightCSS = null; 106 | 107 | doc.addEventListener(COLOR_SCHEME_CHANGE, (event) => { 108 | this.mode = event.detail.colorScheme; 109 | this._updateRadios(); 110 | this._updateCheckbox(); 111 | this._updateThreeWayRadios(); 112 | }); 113 | 114 | doc.addEventListener(PERMANENT_COLOR_SCHEME, (event) => { 115 | this.permanent = event.detail.permanent; 116 | this._permanentCheckbox.checked = this.permanent; 117 | this._updateThreeWayRadios(); 118 | }); 119 | 120 | this._initializeDOM(); 121 | } 122 | 123 | _initializeDOM() { 124 | const shadowRoot = this.attachShadow({ mode: 'open' }); 125 | shadowRoot.append(template.content.cloneNode(true)); 126 | 127 | // We need to support `media="(prefers-color-scheme: dark)"` (with space) 128 | // and `media="(prefers-color-scheme:dark)"` (without space) 129 | this._darkCSS = doc.querySelectorAll( 130 | `${LINK_REL_STYLESHEET}[${MEDIA}*=${PREFERS_COLOR_SCHEME}][${MEDIA}*="${DARK}"], 131 | ${STYLE}[${MEDIA}*=${PREFERS_COLOR_SCHEME}][${MEDIA}*="${DARK}"]`, 132 | ); 133 | this._lightCSS = doc.querySelectorAll( 134 | `${LINK_REL_STYLESHEET}[${MEDIA}*=${PREFERS_COLOR_SCHEME}][${MEDIA}*="${LIGHT}"], 135 | ${STYLE}[${MEDIA}*=${PREFERS_COLOR_SCHEME}][${MEDIA}*="${LIGHT}"]`, 136 | ); 137 | 138 | // Get DOM references. 139 | this._lightRadio = shadowRoot.querySelector('[part=lightRadio]'); 140 | this._lightLabel = shadowRoot.querySelector('[part=lightLabel]'); 141 | this._darkRadio = shadowRoot.querySelector('[part=darkRadio]'); 142 | this._darkLabel = shadowRoot.querySelector('[part=darkLabel]'); 143 | this._darkCheckbox = shadowRoot.querySelector('[part=toggleCheckbox]'); 144 | this._checkboxLabel = shadowRoot.querySelector('[part=toggleLabel]'); 145 | this._lightThreeWayRadio = shadowRoot.querySelector( 146 | '[part=lightThreeWayRadio]', 147 | ); 148 | this._lightThreeWayLabel = shadowRoot.querySelector( 149 | '[part=lightThreeWayLabel]', 150 | ); 151 | this._systemThreeWayRadio = shadowRoot.querySelector( 152 | '[part=systemThreeWayRadio]', 153 | ); 154 | this._systemThreeWayLabel = shadowRoot.querySelector( 155 | '[part=systemThreeWayLabel]', 156 | ); 157 | this._darkThreeWayRadio = shadowRoot.querySelector( 158 | '[part=darkThreeWayRadio]', 159 | ); 160 | this._darkThreeWayLabel = shadowRoot.querySelector( 161 | '[part=darkThreeWayLabel]', 162 | ); 163 | this._legendLabel = shadowRoot.querySelector('legend'); 164 | this._permanentAside = shadowRoot.querySelector('aside'); 165 | this._permanentCheckbox = shadowRoot.querySelector( 166 | '[part=permanentCheckbox]', 167 | ); 168 | this._permanentLabel = shadowRoot.querySelector('[part=permanentLabel]'); 169 | } 170 | 171 | connectedCallback() { 172 | // Does the browser support native `prefers-color-scheme`? 173 | const hasNativePrefersColorScheme = matchMedia(MQ_DARK).media !== NOT_ALL; 174 | // Listen to `prefers-color-scheme` changes. 175 | if (hasNativePrefersColorScheme) { 176 | matchMedia(MQ_DARK).addListener(({ matches }) => { 177 | if (this.permanent) { 178 | return; 179 | } 180 | this.mode = matches ? DARK : LIGHT; 181 | this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode }); 182 | }); 183 | } 184 | // Set initial state, giving preference to a remembered value, then the 185 | // native value (if supported), and eventually defaulting to a light 186 | // experience. 187 | let rememberedValue = false; 188 | try { 189 | rememberedValue = store.getItem(NAME); 190 | } catch (err) { 191 | // Do nothing. The user probably blocks cookies. 192 | } 193 | if (rememberedValue && [DARK, LIGHT].includes(rememberedValue)) { 194 | this.mode = rememberedValue; 195 | this._permanentCheckbox.checked = true; 196 | this.permanent = true; 197 | } else if (hasNativePrefersColorScheme) { 198 | this.mode = matchMedia(MQ_LIGHT).matches ? LIGHT : DARK; 199 | } 200 | if (!this.mode) { 201 | this.mode = LIGHT; 202 | } 203 | if (this.permanent && !rememberedValue) { 204 | try { 205 | store.setItem(NAME, this.mode); 206 | } catch (err) { 207 | // Do nothing. The user probably blocks cookies. 208 | } 209 | } 210 | 211 | // Default to toggle appearance. 212 | if (!this.appearance) { 213 | this.appearance = TOGGLE; 214 | } 215 | 216 | // Update the appearance to toggle, switch or three-way. 217 | this._updateAppearance(); 218 | 219 | // Update the radios 220 | this._updateRadios(); 221 | 222 | // Make the checkbox reflect the state of the radios 223 | this._updateCheckbox(); 224 | 225 | // Make the 3 way radio reflect the state of the radios 226 | this._updateThreeWayRadios(); 227 | 228 | // Synchronize the behavior of the radio and the checkbox. 229 | [this._lightRadio, this._darkRadio].forEach((input) => { 230 | input.addEventListener('change', () => { 231 | this.mode = this._lightRadio.checked ? LIGHT : DARK; 232 | this._updateCheckbox(); 233 | this._updateThreeWayRadios(); 234 | this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode }); 235 | }); 236 | }); 237 | this._darkCheckbox.addEventListener('change', () => { 238 | this.mode = this._darkCheckbox.checked ? DARK : LIGHT; 239 | this._updateRadios(); 240 | this._updateThreeWayRadios(); 241 | this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode }); 242 | }); 243 | this._lightThreeWayRadio.addEventListener('change', () => { 244 | this.mode = LIGHT; 245 | this.permanent = true; 246 | this._updateCheckbox(); 247 | this._updateRadios(); 248 | this._updateThreeWayRadios(); 249 | this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode }); 250 | this._dispatchEvent(PERMANENT_COLOR_SCHEME, { 251 | permanent: this.permanent, 252 | }); 253 | }); 254 | this._darkThreeWayRadio.addEventListener('change', () => { 255 | this.mode = DARK; 256 | this.permanent = true; 257 | this._updateCheckbox(); 258 | this._updateRadios(); 259 | this._updateThreeWayRadios(); 260 | this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode }); 261 | this._dispatchEvent(PERMANENT_COLOR_SCHEME, { 262 | permanent: this.permanent, 263 | }); 264 | }); 265 | this._systemThreeWayRadio.addEventListener('change', () => { 266 | this.mode = this._getPrefersColorScheme(); 267 | this.permanent = false; 268 | this._updateCheckbox(); 269 | this._updateRadios(); 270 | this._updateThreeWayRadios(); 271 | this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode }); 272 | this._dispatchEvent(PERMANENT_COLOR_SCHEME, { 273 | permanent: this.permanent, 274 | }); 275 | }); 276 | // Make remembering the last mode optional 277 | this._permanentCheckbox.addEventListener('change', () => { 278 | this.permanent = this._permanentCheckbox.checked; 279 | this._updateThreeWayRadios(); 280 | this._dispatchEvent(PERMANENT_COLOR_SCHEME, { 281 | permanent: this.permanent, 282 | }); 283 | }); 284 | 285 | // Finally update the mode and let the world know what's going on 286 | this._updateMode(); 287 | this._dispatchEvent(COLOR_SCHEME_CHANGE, { colorScheme: this.mode }); 288 | this._dispatchEvent(PERMANENT_COLOR_SCHEME, { 289 | permanent: this.permanent, 290 | }); 291 | } 292 | 293 | attributeChangedCallback(name, oldValue, newValue) { 294 | if (name === MODE) { 295 | const allAttributes = [LIGHT, SYSTEM, DARK]; 296 | if (!allAttributes.includes(newValue)) { 297 | throw new RangeError( 298 | `Allowed values are: "${allAttributes.join(`", "`)}".`, 299 | ); 300 | } 301 | // Only show the dialog programmatically on devices not capable of hover 302 | // and only if there is a label 303 | if (matchMedia('(hover:none)').matches && this.remember) { 304 | this._showPermanentAside(); 305 | } 306 | if (this.permanent) { 307 | try { 308 | store.setItem(NAME, this.mode); 309 | } catch (err) { 310 | // Do nothing. The user probably blocks cookies. 311 | } 312 | } 313 | this._updateRadios(); 314 | this._updateCheckbox(); 315 | this._updateThreeWayRadios(); 316 | this._updateMode(); 317 | } else if (name === APPEARANCE) { 318 | const allAppearanceOptions = [TOGGLE, SWITCH, THREE_WAY]; 319 | if (!allAppearanceOptions.includes(newValue)) { 320 | throw new RangeError( 321 | `Allowed values are: "${allAppearanceOptions.join(`", "`)}".`, 322 | ); 323 | } 324 | this._updateAppearance(); 325 | } else if (name === PERMANENT) { 326 | if (this.permanent) { 327 | if (this.mode) { 328 | try { 329 | store.setItem(NAME, this.mode); 330 | } catch (err) { 331 | // Do nothing. The user probably blocks cookies. 332 | } 333 | } 334 | } else { 335 | try { 336 | store.removeItem(NAME); 337 | } catch (err) { 338 | // Do nothing. The user probably blocks cookies. 339 | } 340 | } 341 | this._permanentCheckbox.checked = this.permanent; 342 | } else if (name === LEGEND) { 343 | this._legendLabel.textContent = newValue; 344 | } else if (name === REMEMBER) { 345 | this._permanentLabel.textContent = newValue; 346 | } else if (name === LIGHT) { 347 | this._lightLabel.textContent = newValue; 348 | if (this.mode === LIGHT) { 349 | this._checkboxLabel.textContent = newValue; 350 | } 351 | } else if (name === DARK) { 352 | this._darkLabel.textContent = newValue; 353 | if (this.mode === DARK) { 354 | this._checkboxLabel.textContent = newValue; 355 | } 356 | } 357 | } 358 | 359 | _getPrefersColorScheme() { 360 | return matchMedia(MQ_LIGHT).matches ? LIGHT : DARK; 361 | } 362 | 363 | _dispatchEvent(type, value) { 364 | this.dispatchEvent( 365 | new CustomEvent(type, { 366 | bubbles: true, 367 | composed: true, 368 | detail: value, 369 | }), 370 | ); 371 | } 372 | 373 | _updateAppearance() { 374 | // Hide or show the light-related affordances dependent on the appearance, 375 | // which can be "switch" , "toggle" or "three-way". 376 | this._lightRadio.hidden = 377 | this._lightLabel.hidden = 378 | this._darkRadio.hidden = 379 | this._darkLabel.hidden = 380 | this._darkCheckbox.hidden = 381 | this._checkboxLabel.hidden = 382 | this._lightThreeWayRadio.hidden = 383 | this._lightThreeWayLabel.hidden = 384 | this._systemThreeWayRadio.hidden = 385 | this._systemThreeWayLabel.hidden = 386 | this._darkThreeWayRadio.hidden = 387 | this._darkThreeWayLabel.hidden = 388 | true; 389 | switch (this.appearance) { 390 | case SWITCH: 391 | this._lightRadio.hidden = 392 | this._lightLabel.hidden = 393 | this._darkRadio.hidden = 394 | this._darkLabel.hidden = 395 | false; 396 | break; 397 | case THREE_WAY: 398 | this._lightThreeWayRadio.hidden = 399 | this._lightThreeWayLabel.hidden = 400 | this._systemThreeWayRadio.hidden = 401 | this._systemThreeWayLabel.hidden = 402 | this._darkThreeWayRadio.hidden = 403 | this._darkThreeWayLabel.hidden = 404 | false; 405 | break; 406 | case TOGGLE: 407 | default: 408 | this._darkCheckbox.hidden = this._checkboxLabel.hidden = false; 409 | break; 410 | } 411 | } 412 | 413 | _updateRadios() { 414 | if (this.mode === LIGHT) { 415 | this._lightRadio.checked = true; 416 | } else { 417 | this._darkRadio.checked = true; 418 | } 419 | } 420 | 421 | _updateCheckbox() { 422 | if (this.mode === LIGHT) { 423 | this._checkboxLabel.style.setProperty( 424 | `--${NAME}-checkbox-icon`, 425 | `var(--${NAME}-light-icon,url("${DEFAULT_URL}moon.png"))`, 426 | ); 427 | this._checkboxLabel.textContent = this.light; 428 | if (!this.light) { 429 | this._checkboxLabel.ariaLabel = DARK; 430 | } 431 | this._darkCheckbox.checked = false; 432 | } else { 433 | this._checkboxLabel.style.setProperty( 434 | `--${NAME}-checkbox-icon`, 435 | `var(--${NAME}-dark-icon,url("${DEFAULT_URL}sun.png"))`, 436 | ); 437 | this._checkboxLabel.textContent = this.dark; 438 | if (!this.dark) { 439 | this._checkboxLabel.ariaLabel = LIGHT; 440 | } 441 | this._darkCheckbox.checked = true; 442 | } 443 | } 444 | 445 | _updateThreeWayRadios() { 446 | this._lightThreeWayLabel.ariaLabel = LIGHT; 447 | this._systemThreeWayLabel.ariaLabel = SYSTEM; 448 | this._darkThreeWayLabel.ariaLabel = DARK; 449 | this._lightThreeWayLabel.textContent = this.light; 450 | this._systemThreeWayLabel.textContent = this.system; 451 | this._darkThreeWayLabel.textContent = this.dark; 452 | if (this.permanent) { 453 | if (this.mode === LIGHT) { 454 | this._lightThreeWayRadio.checked = true; 455 | } else { 456 | this._darkThreeWayRadio.checked = true; 457 | } 458 | } else { 459 | this._systemThreeWayRadio.checked = true; 460 | } 461 | } 462 | 463 | _updateMode() { 464 | if (this.mode === LIGHT) { 465 | this._lightCSS.forEach((link) => { 466 | link.media = ALL; 467 | link.disabled = false; 468 | }); 469 | this._darkCSS.forEach((link) => { 470 | link.media = NOT_ALL; 471 | link.disabled = true; 472 | }); 473 | } else { 474 | this._darkCSS.forEach((link) => { 475 | link.media = ALL; 476 | link.disabled = false; 477 | }); 478 | this._lightCSS.forEach((link) => { 479 | link.media = NOT_ALL; 480 | link.disabled = true; 481 | }); 482 | } 483 | } 484 | 485 | _showPermanentAside() { 486 | this._permanentAside.style.visibility = 'visible'; 487 | setTimeout(() => { 488 | this._permanentAside.style.visibility = 'hidden'; 489 | }, 3000); 490 | } 491 | } 492 | 493 | customElements.define(NAME, DarkModeToggle); 494 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `` Element 2 | 3 | [![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/dark-mode-toggle) 4 | 5 | A custom element that allows you to easily put a _Dark Mode 🌒_ toggle or switch 6 | on your site, so you can initially adhere to your users' preferences according 7 | to 8 | [`prefers-color-scheme`](https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme), 9 | but also allow them to (optionally permanently) override their system setting 10 | for just your site. 11 | 12 | 📚 Read all(!) about dark mode in the related article 13 | [Hello Darkness, My Old Friend](https://web.dev/prefers-color-scheme/). 14 | 15 | ## Installation 16 | 17 | Install from npm: 18 | 19 | ```bash 20 | npm install --save dark-mode-toggle 21 | ``` 22 | 23 | Or, alternatively, use a ` 27 | ``` 28 | 29 | ![Dark mode toggle live coding sample.](https://user-images.githubusercontent.com/145676/94532333-0466b580-023e-11eb-947e-f73044a7cd63.gif) 30 | 31 | (See the [original HD version](https://youtu.be/qfsvoPhx-bE) so you can pause.) 32 | 33 | ## Usage 34 | 35 | There are three ways how you can use ``: 36 | 37 | ### ① Using different stylesheets per color scheme that are conditionally loaded 38 | 39 | The custom element assumes that you have organized your CSS in different files 40 | that you load conditionally based on the **`media`** attribute in the 41 | stylesheet's corresponding `link` element. This is a great performance pattern, 42 | as you don't force people to download CSS that they don't need based on their 43 | current theme preference, yet non-matching stylesheets still get loaded, but 44 | don't compete for bandwidth in the critical rendering path. You can also have 45 | more than one file per theme. The example below illustrates the principle. 46 | 47 | 75 | 76 | ```html 77 | 78 | 79 | 84 | 85 | 89 | 90 | 91 |
92 |

Hi there

93 | Sitting cat in front of a tree 99 |

Check out the dark mode toggle in the upper right corner!

100 |
101 | 111 | ``` 112 | 113 | The above method might cause flashing 114 | ([#77](https://github.com/GoogleChromeLabs/dark-mode-toggle/issues/77)) when the 115 | page loads, as the dark mode toggle module is loaded after the page is rendered. 116 | A loader script can be used to apply the saved theme before the page is 117 | rendered. Wrap the stylesheet tags with 118 | `` and add the loader 119 | script as follows: 120 | 121 | ```html 122 | 123 | 124 | 137 | 138 | 142 | 143 | 144 | ``` 145 | 146 | ### ② Using a CSS class that you toggle 147 | 148 | If you prefer to not split your CSS in different files based on the color 149 | scheme, you can instead work with a class that you toggle, for example 150 | `class="dark"`. You can see this in action in 151 | [this demo](https://googlechrome.github.io/samples/dark-mode-class-toggle/). 152 | 153 | ```js 154 | import * as DarkModeToggle from 'https://googlechromelabs.github.io/dark-mode-toggle/src/dark-mode-toggle.mjs'; 155 | 156 | const toggle = document.querySelector('dark-mode-toggle'); 157 | const body = document.body; 158 | 159 | // Set or remove the `dark` class the first time. 160 | toggle.mode === 'dark' 161 | ? body.classList.add('dark') 162 | : body.classList.remove('dark'); 163 | 164 | // Listen for toggle changes (which includes `prefers-color-scheme` changes) 165 | // and toggle the `dark` class accordingly. 166 | toggle.addEventListener('colorschemechange', () => { 167 | body.classList.toggle('dark', toggle.mode === 'dark'); 168 | }); 169 | ``` 170 | 171 | ### ③ Using internal stylesheets for each color scheme 172 | 173 | This approach allows you to define styles directly within your HTML using 174 | ` 188 | 194 | 198 | 199 | 200 | ``` 201 | 202 | ## Demo 203 | 204 | See the custom element in action in the 205 | [interactive demo](https://googlechromelabs.github.io/dark-mode-toggle/demo/index.html). 206 | It shows four different kinds of synchronized ``s. If you use 207 | Chrome on an Android device, pay attention to the address bar's theme color, and 208 | also note how the favicon changes. 209 | 210 | Dark 211 | Light 212 | 213 | ## Properties 214 | 215 | Properties can be set directly on the custom element at creation time, or 216 | dynamically via JavaScript. 217 | 218 | 👉 Note that the dark and light **icons** are set via CSS variables, see 219 | [Style Customization](#style-customization) below. 220 | 221 | | Name | Required | Values | Default | Description | 222 | | ------------ | -------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | 223 | | `mode` | No | Any of `"dark"` or `"light"` | Defaults to whatever the user's preferred color scheme is according to `prefers-color-scheme`, or `"light"` if the user's browser doesn't support the media query. | If set overrides the user's preferred color scheme. | 224 | | `appearance` | No | Any of `"toggle"` or `"switch"` | Defaults to `"toggle"`. | The `"switch"` appearance conveys the idea of a theme switcher (light/dark), whereas `"toggle"` conveys the idea of a dark mode toggle (on/off). | 225 | | `permanent` | No | `true` if present | Defaults to not remember the last choice. | If present remembers the last selected mode (`"dark"` or `"light"`), which allows the user to permanently override their usual preferred color scheme. | 226 | | `legend` | No | Any string | Defaults to no legend. | Any string value that represents the legend for the toggle or switch. | 227 | | `light` | No | Any string | Defaults to no label. | Any string value that represents the label for the `"light"` mode. | 228 | | `dark` | No | Any string | Defaults to no label. | Any string value that represents the label for the `"dark"` mode. | 229 | | `remember` | No | Any string | Defaults to no label. | Any string value that represents the label for the "remember the last selected mode" functionality. | 230 | 231 | ## Events 232 | 233 | - `colorschemechange`: Fired when the color scheme gets changed. 234 | - `permanentcolorscheme`: Fired when the color scheme should be permanently 235 | remembered or not. 236 | 237 | ## Complete Example 238 | 239 | Interacting with the custom element: 240 | 241 | ```js 242 | /* On the page */ 243 | const darkModeToggle = document.querySelector('dark-mode-toggle'); 244 | 245 | // Set the mode to dark 246 | darkModeToggle.mode = 'dark'; 247 | // Set the mode to light 248 | darkModeToggle.mode = 'light'; 249 | 250 | // Set the legend to "Dark Mode" 251 | darkModeToggle.legend = 'Dark Mode'; 252 | // Set the light label to "off" 253 | darkModeToggle.light = 'off'; 254 | // Set the dark label to "on" 255 | darkModeToggle.dark = 'on'; 256 | 257 | // Set the appearance to resemble a switch (theme: light/dark) 258 | darkModeToggle.appearance = 'switch'; 259 | // Set the appearance to resemble a toggle (dark mode: on/off) 260 | darkModeToggle.appearance = 'toggle'; 261 | 262 | // Set a "remember the last selected mode" label 263 | darkModeToggle.remember = 'Remember this'; 264 | 265 | // Remember the user's last color scheme choice 266 | darkModeToggle.setAttribute('permanent', ''); 267 | // Forget the user's last color scheme choice 268 | darkModeToggle.removeAttribute('permanent'); 269 | ``` 270 | 271 | Reacting on color scheme changes: 272 | 273 | ```js 274 | /* On the page */ 275 | document.addEventListener('colorschemechange', (e) => { 276 | console.log(`Color scheme changed to ${e.detail.colorScheme}.`); 277 | }); 278 | ``` 279 | 280 | Reacting on "remember the last selected mode" functionality changes: 281 | 282 | ```js 283 | /* On the page */ 284 | document.addEventListener('permanentcolorscheme', (e) => { 285 | console.log( 286 | `${e.detail.permanent ? 'R' : 'Not r'}emembering the last selected mode.`, 287 | ); 288 | }); 289 | ``` 290 | 291 | ## Style Customization 292 | 293 | You can style the custom element with 294 | [`::part()`](https://developer.mozilla.org/en-US/docs/Web/CSS/::part). See the 295 | demo's 296 | [CSS source code](https://github.com/GoogleChromeLabs/dark-mode-toggle/blob/master/demo/common.css) 297 | for some concrete examples. The exposed parts and their names can be seen below: 298 | 299 | ```html 300 |
301 |
302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 313 |
314 |
315 | ``` 316 | 317 | Additionally, you can use a number of exposed CSS variables, as listed in the 318 | following: 319 | 320 | | CSS Variable Name | Default | Description | 321 | | ------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 322 | | `--dark-mode-toggle-light-icon` | No icon | The icon for the light state in `background-image:` notation. | 323 | | `--dark-mode-toggle-dark-icon` | No icon | The icon for the dark state in `background-image:` notation. | 324 | | `--dark-mode-toggle-icon-size` | 1rem | The icon size in CSS length data type notation. | 325 | | `--dark-mode-toggle-remember-icon-checked` | No icon | The icon for the checked "remember the last selected mode" functionality in `background-image:` notation. | 326 | | `--dark-mode-toggle-remember-icon-unchecked` | No icon | The icon for the unchecked "remember the last selected mode" functionality in `background-image:` notation. | 327 | | `--dark-mode-toggle-color` | User-Agent stylesheet text color | The main text color in `color:` notation. | 328 | | `--dark-mode-toggle-background-color` | User-Agent stylesheet background color | The main background color in `background-color:` notation. | 329 | | `--dark-mode-toggle-legend-font` | User-Agent `` font | The font of the legend in shorthand `font:` notation. | 330 | | `--dark-mode-toggle-label-font` | User-Agent `