├── .gitignore ├── DarkMode.jpg ├── .stylelintrc.json ├── package.json ├── LICENSE ├── main.js ├── README.md ├── main.css ├── main.scss └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /DarkMode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royalfig/dark-mode-demo/HEAD/DarkMode.jpg -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recess-order", 4 | "stylelint-config-recommended-scss" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dark-mode-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "sass-watch": "node-sass -w --output-style expanded main.scss main.css", 9 | "sl": "npx stylelint main.scss" 10 | }, 11 | "author": "Ryan Feigenbaum", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "node-sass": "^4.13.1", 15 | "stylelint": "^13.2.1", 16 | "stylelint-config-recess-order": "^2.0.4", 17 | "stylelint-config-recommended-scss": "^4.2.0", 18 | "stylelint-config-sass-guidelines": "^7.0.0", 19 | "stylelint-config-standard": "^20.0.0", 20 | "stylelint-order": "^4.0.0", 21 | "stylelint-scss": "^3.15.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ryan Feigenbaum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | if (window.CSS && CSS.supports("color", "var(--primary)")) { 2 | var toggleColorMode = function toggleColorMode(e) { 3 | // Switch to Light Mode 4 | if (e.currentTarget.classList.contains("light--hidden")) { 5 | // Sets the custom html attribute 6 | document.documentElement.setAttribute("color-mode", "light"); // Sets the user's preference in local storage 7 | 8 | localStorage.setItem("color-mode", "light"); 9 | return; 10 | } 11 | /* Switch to Dark Mode 12 | Sets the custom html attribute */ 13 | document.documentElement.setAttribute("color-mode", "dark"); // Sets the user's preference in local storage 14 | 15 | localStorage.setItem("color-mode", "dark"); 16 | }; // Get the buttons in the DOM 17 | 18 | var toggleColorButtons = document.querySelectorAll(".color-mode__btn"); // Set up event listeners 19 | 20 | toggleColorButtons.forEach(function(btn) { 21 | btn.addEventListener("click", toggleColorMode); 22 | }); 23 | } else { 24 | // If the feature isn't supported, then we hide the toggle buttons 25 | var btnContainer = document.querySelector(".color-mode__header"); 26 | btnContainer.style.display = "none"; 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](DarkMode.jpg)](https://royalfig.github.io/dark-mode-demo/) 2 | 3 | # The Complete Guide to the Dark Mode Toggle 4 | 5 | You may have noticed that Dark Mode is becoming more and more a thing. In this article, I provide a complete guide to adding Dark Mode to your website. 6 | 7 | > Best Practices to Unlock the Dark Side 8 | 9 | Recently, Chris Coyier at CSS-Tricks threw down the gauntlet for anyone who was about "to write a blog post about dark mode." He listed 10 points that would need to be covered. 10 | 11 | - Explain Dark Mode 12 | - Provide a demo 13 | - Explain that Dark Mode can happen at the operating system level itself 14 | - Show how JavaScript can know about the OS-level choice 15 | - Let the user have ultimate say over color preference 16 | - Build a theme switcher, including gotchas 17 | - See who else is building Dark Mode toggles 18 | - See who else is writing about it 19 | - Discuss what is and isn't supported 20 | - Make accessibility a main concern 21 | 22 | In my post, "[The Complete Guide to the Dark Mode Toggle](https://ryanfeigenbaum.com/dark-mode)," I aim to cover all 10 points (covering some better than others). I offer what I've found to be the best practices for implementing a dark mode toggle, and this repo serves a demo for those best practices. 23 | 24 | Check out the [Dark Mode Toggle demo](https://royalfig.github.io/dark-mode-demo/). 25 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --radius: 50px; 3 | --sans: -apple-system, blinkmacsystemfont, "Segoe UI", roboto, oxygen, ubuntu, 4 | cantarell, "Open Sans", "Helvetica Neue", sans-serif; 5 | } 6 | 7 | :root[color-mode="light"] { 8 | --surface1: #e6e6e6; 9 | --surface2: #f2f2f2; 10 | --surface3: #ffffff; 11 | --element1: #111111; 12 | --element2: #222222; 13 | --element3: #333333; 14 | --elementInverse: #eee; 15 | --primary: #01408e; 16 | --secondary: #3c5d5c; 17 | --tertiary: #fff7d6; 18 | --box-shadow: 20px 20px 60px #cacaca, -20px -20px 60px #ffffff; 19 | } 20 | 21 | :root[color-mode="dark"] { 22 | --surface1: #262626; 23 | --surface2: #333333; 24 | --surface3: #404040; 25 | --element1: #eeeeee; 26 | --element2: #dddddd; 27 | --element3: #cccccc; 28 | --elementInverse: #111; 29 | --primary: #8fceff; 30 | --secondary: #72faca; 31 | --tertiary: #eee8a9; 32 | --box-shadow: 20px 20px 60px #1d1d1d, -20px -20px 60px #272727; 33 | } 34 | 35 | html { 36 | padding: 0; 37 | margin: 0; 38 | font-family: var(--sans); 39 | font-size: 1rem; 40 | line-height: 1.5; 41 | background-color: #e6e6e6; 42 | background-color: var(--surface1, #e6e6e6); 43 | } 44 | 45 | body { 46 | padding: 0; 47 | margin: 0; 48 | } 49 | 50 | .color-mode { 51 | display: flex; 52 | flex-direction: column; 53 | align-items: center; 54 | justify-content: center; 55 | min-height: calc(100vh - 4rem); 56 | padding: 1rem 0.5rem; 57 | margin: 0; 58 | } 59 | 60 | :root[color-mode="light"] .color-mode .light--hidden { 61 | display: none; 62 | } 63 | 64 | :root[color-mode="dark"] .color-mode .dark--hidden { 65 | display: none; 66 | } 67 | 68 | @media (min-width: 640px) { 69 | .color-mode { 70 | padding: 2rem; 71 | } 72 | } 73 | 74 | .color-mode__header { 75 | position: relative; 76 | padding: 1rem 0; 77 | border-radius: var(--radius); 78 | } 79 | 80 | .color-mode__header:after { 81 | position: absolute; 82 | bottom: 0; 83 | left: 0; 84 | width: 100%; 85 | height: 5px; 86 | content: ""; 87 | background: linear-gradient(to right, #01408e, #3c5d5c); 88 | background: linear-gradient(to right, var(--primary, #01408e), var(--secondary, #3c5d5c)); 89 | } 90 | 91 | .color-mode__btn { 92 | display: flex; 93 | align-items: center; 94 | justify-content: center; 95 | padding: 2rem; 96 | margin: 0 auto 1.5rem; 97 | font-family: var(--sans); 98 | font-size: 1rem; 99 | font-weight: 600; 100 | line-height: 1; 101 | color: #111111; 102 | color: var(--element1, #111111); 103 | cursor: pointer; 104 | background: none; 105 | border: none; 106 | border-radius: var(--radius); 107 | box-shadow: var(--box-shadow); 108 | } 109 | 110 | .color-mode__btn svg { 111 | width: 30px; 112 | height: 30px; 113 | margin-left: 7px; 114 | fill: none; 115 | stroke: #222222; 116 | stroke: var(--element2, #222222); 117 | stroke-linecap: round; 118 | stroke-linejoin: round; 119 | stroke-width: 1.5px; 120 | } 121 | 122 | .color-mode__btn:hover svg, 123 | .color-mode__btn:focus svg, .color-mode__btn:focus { 124 | outline: none; 125 | fill: #fff7d6; 126 | fill: var(--tertiary, #fff7d6); 127 | } 128 | 129 | .color-mode__section { 130 | max-width: 640px; 131 | padding: 0.5rem 1rem; 132 | margin-bottom: 5rem; 133 | color: #111111; 134 | color: var(--element1, #111111); 135 | background-color: #e6e6e6; 136 | background-color: var(--surface1, #e6e6e6); 137 | border-radius: var(--radius); 138 | box-shadow: var(--box-shadow); 139 | transition: all 0.2s ease-in; 140 | } 141 | 142 | @media (min-width: 640px) { 143 | .color-mode__section { 144 | padding: 1rem 3rem; 145 | } 146 | } 147 | 148 | .color-mode h1 { 149 | font-size: 2.5rem; 150 | line-height: 1.1; 151 | } 152 | 153 | .color-mode h2 { 154 | color: #222222; 155 | color: var(--element2, #222222); 156 | } 157 | 158 | .color-mode__link-container { 159 | display: flex; 160 | width: 100%; 161 | margin: 2rem 0; 162 | } 163 | 164 | .color-mode__link { 165 | padding: 0.5rem 1rem; 166 | font-weight: 600; 167 | color: #111111; 168 | color: var(--element1, #111111); 169 | text-decoration: none; 170 | background-color: #e6e6e6; 171 | background-color: var(--surface1, #e6e6e6); 172 | border-color: #01408e; 173 | border-color: var(--primary, #01408e); 174 | border-style: solid; 175 | border-width: 2px; 176 | border-radius: var(--radius); 177 | transition: all 0.2s ease-in; 178 | } 179 | 180 | .color-mode__link:hover, .color-mode__link:focus { 181 | color: #eee; 182 | color: var(--elementInverse, #eee); 183 | background-color: #01408e; 184 | background-color: var(--primary, #01408e); 185 | } 186 | 187 | .color-mode__excerpt { 188 | padding: 0.5rem 1.5rem; 189 | margin: 1rem; 190 | color: #222222; 191 | color: var(--element2, #222222); 192 | background-color: #f2f2f2; 193 | background-color: var(--surface2, #f2f2f2); 194 | border-radius: var(--radius); 195 | } 196 | 197 | @media (min-width: 640px) { 198 | .color-mode__excerpt { 199 | padding: 1rem 2rem; 200 | margin: 2rem; 201 | } 202 | } 203 | 204 | .color-mode__excerpt a { 205 | color: #01408e; 206 | color: var(--primary, #01408e); 207 | } 208 | 209 | .color-mode__excerpt a:hover, .color-mode__excerpt a:focus { 210 | color: #3c5d5c; 211 | color: var(--secondary, #3c5d5c); 212 | } 213 | -------------------------------------------------------------------------------- /main.scss: -------------------------------------------------------------------------------- 1 | $colors: ( 2 | "surface1": #e6e6e6, 3 | "surface2": #f2f2f2, 4 | "surface3": #ffffff, 5 | "element1": #111111, 6 | "element2": #222222, 7 | "element3": #333333, 8 | "elementInverse": #eee, 9 | "primary": #01408e, 10 | "secondary": #3c5d5c, 11 | "tertiary": #fff7d6 12 | ); 13 | 14 | @mixin color-var($property, $color) { 15 | #{$property}: map-get($colors, "#{$color}"); 16 | #{$property}: var(--#{$color}, map-get($colors, "#{$color}")); 17 | } 18 | 19 | :root { 20 | --radius: 50px; 21 | --sans: -apple-system, blinkmacsystemfont, "Segoe UI", roboto, oxygen, ubuntu, 22 | cantarell, "Open Sans", "Helvetica Neue", sans-serif; 23 | 24 | &[color-mode="light"] { 25 | --surface1: #e6e6e6; 26 | --surface2: #f2f2f2; 27 | --surface3: #ffffff; 28 | --element1: #111111; 29 | --element2: #222222; 30 | --element3: #333333; 31 | --elementInverse: #eee; 32 | --primary: #01408e; 33 | --secondary: #3c5d5c; 34 | --tertiary: #fff7d6; 35 | --box-shadow: 20px 20px 60px #cacaca, -20px -20px 60px #ffffff; 36 | } 37 | 38 | &[color-mode="dark"] { 39 | --surface1: #262626; 40 | --surface2: #333333; 41 | --surface3: #404040; 42 | --element1: #eeeeee; 43 | --element2: #dddddd; 44 | --element3: #cccccc; 45 | --elementInverse: #111; 46 | --primary: #8fceff; 47 | --secondary: #72faca; 48 | --tertiary: #eee8a9; 49 | --box-shadow: 20px 20px 60px #1d1d1d, -20px -20px 60px #272727; 50 | } 51 | } 52 | 53 | html { 54 | padding: 0; 55 | margin: 0; 56 | font-family: var(--sans); 57 | font-size: 1rem; 58 | line-height: 1.5; 59 | @include color-var(background-color, surface1); 60 | } 61 | 62 | body { 63 | padding: 0; 64 | margin: 0; 65 | } 66 | .color-mode { 67 | :root[color-mode="light"] & { 68 | .light--hidden { 69 | display: none; 70 | } 71 | } 72 | :root[color-mode="dark"] & { 73 | .dark--hidden { 74 | display: none; 75 | } 76 | } 77 | 78 | display: flex; 79 | flex-direction: column; 80 | align-items: center; 81 | justify-content: center; 82 | min-height: calc(100vh - 4rem); 83 | padding: 1rem 0.5rem; 84 | 85 | @media (min-width: 640px) { 86 | padding: 2rem; 87 | } 88 | 89 | margin: 0; 90 | 91 | &__header { 92 | position: relative; 93 | padding: 1rem 0; 94 | &:after { 95 | position: absolute; 96 | bottom: 0; 97 | left: 0; 98 | width: 100%; 99 | height: 5px; 100 | content: ""; 101 | background: linear-gradient( 102 | to right, 103 | map-get($colors, primary), 104 | map-get($colors, secondary) 105 | ); 106 | background: linear-gradient( 107 | to right, 108 | var(--primary, map-get($colors, primary)), 109 | var(--secondary, map-get($colors, secondary)) 110 | ); 111 | } 112 | border-radius: var(--radius); 113 | } 114 | 115 | &__btn { 116 | display: flex; 117 | align-items: center; 118 | justify-content: center; 119 | padding: 2rem; 120 | margin: 0 auto 1.5rem; 121 | font-family: var(--sans); 122 | font-size: 1rem; 123 | font-weight: 600; 124 | line-height: 1; 125 | @include color-var(color, element1); 126 | cursor: pointer; 127 | background: none; 128 | border: none; 129 | border-radius: var(--radius); 130 | box-shadow: var(--box-shadow); 131 | 132 | svg { 133 | width: 30px; 134 | height: 30px; 135 | margin-left: 7px; 136 | fill: none; 137 | @include color-var(stroke, element2); 138 | stroke-linecap: round; 139 | stroke-linejoin: round; 140 | stroke-width: 1.5px; 141 | } 142 | 143 | &:hover svg, 144 | &:focus svg, 145 | &:focus { 146 | outline: none; 147 | @include color-var(fill, tertiary); 148 | } 149 | } 150 | 151 | &__section { 152 | max-width: 640px; 153 | padding: 0.5rem 1rem; 154 | 155 | @media (min-width: 640px) { 156 | padding: 1rem 3rem; 157 | } 158 | 159 | margin-bottom: 5rem; 160 | @include color-var(color, element1); 161 | @include color-var(background-color, surface1); 162 | border-radius: var(--radius); 163 | box-shadow: var(--box-shadow); 164 | transition: all 0.2s ease-in; 165 | } 166 | 167 | h1 { 168 | font-size: 2.5rem; 169 | line-height: 1.1; 170 | } 171 | 172 | h2 { 173 | @include color-var(color, element2); 174 | } 175 | 176 | &__link-container { 177 | display: flex; 178 | width: 100%; 179 | margin: 2rem 0; 180 | } 181 | 182 | &__link { 183 | padding: 0.5rem 1rem; 184 | font-weight: 600; 185 | @include color-var(color, element1); 186 | text-decoration: none; 187 | @include color-var(background-color, surface1); 188 | @include color-var(border-color, primary); 189 | border-style: solid; 190 | border-width: 2px; 191 | border-radius: var(--radius); 192 | transition: all 0.2s ease-in; 193 | &:hover, 194 | &:focus { 195 | @include color-var(color, elementInverse); 196 | @include color-var(background-color, primary); 197 | } 198 | } 199 | 200 | &__excerpt { 201 | padding: 0.5rem 1.5rem; 202 | margin: 1rem; 203 | 204 | @media (min-width: 640px) { 205 | padding: 1rem 2rem; 206 | margin: 2rem; 207 | } 208 | 209 | @include color-var(color, element2); 210 | @include color-var(background-color, surface2); 211 | border-radius: var(--radius); 212 | 213 | a { 214 | @include color-var(color, primary); 215 | 216 | &:hover, 217 | &:focus { 218 | @include color-var(color, secondary); 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dark Mode Demo 7 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 | 48 | 58 |
59 |
60 |

Hi, I'm light mode.

61 | 69 |

Chapter 1

70 |

71 | Alice was beginning to get very tired of sitting by her sister on 72 | the bank, and of having nothing to do: once or twice she had peeped 73 | into the book her sister was reading, but it had no pictures or 74 | conversations in it, “and what is the use of a book,” thought Alice 75 | “without pictures or conversations?” 76 |

77 |

78 | So she was considering in her own mind (as well as she could, for 79 | the hot day made her feel very sleepy and stupid), whether the 80 | pleasure of making a daisy-chain would be worth the trouble of 81 | getting up and picking the daisies, when suddenly a White Rabbit 82 | with pink eyes ran close by her. 83 |

84 |

85 | There was nothing so very remarkable in that; nor did Alice think it 86 | so very much out of the way to hear the Rabbit say to itself, “Oh 87 | dear! Oh dear! I shall be late!” (when she thought it over 88 | afterwards, it occurred to her that she ought to have wondered at 89 | this, but at the time it all seemed quite natural); but when the 90 | Rabbit actually took a watch out of its waistcoat-pocket, and looked 91 | at it, and then hurried on, Alice started to her feet, for it 92 | flashed across her mind that she had never before seen a rabbit with 93 | either a waistcoat-pocket, or a watch to take out of it, and burning 94 | with curiosity, she ran across the field after it, and fortunately 95 | was just in time to see it pop down a large rabbit-hole under the 96 | hedge. 97 |

98 |

99 | 👆 This light excerpt is from Lewis Carroll's 100 | Alice in Wonderland 105 | (1865) 106 |

107 |
108 |
109 |

Hi, I'm dark mode.

110 | 118 |

Chapter 1

119 |

120 | I am by birth a Genevese, and my family is one of the most 121 | distinguished of that republic. My ancestors had been for many years 122 | counsellors and syndics, and my father had filled several public 123 | situations with honour and reputation. He was respected by all who 124 | knew him for his integrity and indefatigable attention to public 125 | business. He passed his younger days perpetually occupied by the 126 | affairs of his country; a variety of circumstances had prevented his 127 | marrying early, nor was it until the decline of life that he became 128 | a husband and the father of a family. 129 |

130 |

131 | As the circumstances of his marriage illustrate his character, I 132 | cannot refrain from relating them. One of his most intimate friends 133 | was a merchant who, from a flourishing state, fell, through numerous 134 | mischances, into poverty. This man, whose name was Beaufort, was of 135 | a proud and unbending disposition and could not bear to live in 136 | poverty and oblivion in the same country where he had formerly been 137 | distinguished for his rank and magnificence. Having paid his debts, 138 | therefore, in the most honourable manner, he retreated with his 139 | daughter to the town of Lucerne, where he lived unknown and in 140 | wretchedness. My father loved Beaufort with the truest friendship 141 | and was deeply grieved by his retreat in these unfortunate 142 | circumstances. He bitterly deplored the false pride which led his 143 | friend to a conduct so little worthy of the affection that united 144 | them. He lost no time in endeavouring to seek him out, with the hope 145 | of persuading him to begin the world again through his credit and 146 | assistance. 147 |

148 | 149 |

150 | 👆 This dark excerpt is 151 | from Mary Shelley's 152 | Frankenstein 157 | (1818) 158 |

159 |
160 |
161 |
162 | 163 | 164 | 165 | --------------------------------------------------------------------------------