├── .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 | [](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 |
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 |
--------------------------------------------------------------------------------