|
| 1 | +Ever thought "ewww this site is so bright?!" and then searched for the "dark mode" button? I'm one of those people too, and despite my love of bright blocks of colour I decided that implementing a dark mode switch would be a good idea. |
| 2 | + |
| 3 | +Additionally, I hate it when things _default_ to light mode, even though my machine is set to dark mode! So in this little guide, I'll walk you through how I implemented a machine-aware dark mode for this site. |
| 4 | + |
| 5 | +## What's the plan? |
| 6 | + |
| 7 | +As this site is very simple and static (no server-side rendering here, just good ol' fashioned HTML and CSS), I'll need to implement a client-side-only dark mode. This means I'll need to: |
| 8 | + |
| 9 | + - Figure out how I want to "apply" a dark mode across the site using CSS |
| 10 | + - Figure out the JavaScript to enable this dark mode |
| 11 | + |
| 12 | +## How to apply dark mode in HTML |
| 13 | + |
| 14 | +This one's quite simple, and I didn't want to muck around too much. As my site is powered by basic CSS (no CSS-in-JS or anything of that ilk), I can just apply a class to the `<body>` element, and then select on that! |
| 15 | + |
| 16 | +So if it's in light mode, the body will just be: |
| 17 | + |
| 18 | +```html |
| 19 | +<body> |
| 20 | + <h1>Content</h1> |
| 21 | +</body> |
| 22 | +``` |
| 23 | + |
| 24 | +But in dark mode, it'll be: |
| 25 | + |
| 26 | +```html |
| 27 | +<body class="dark"> |
| 28 | + <h1>Content</h1> |
| 29 | +</body> |
| 30 | +``` |
| 31 | + |
| 32 | +That means in CSS I can then do things like: |
| 33 | + |
| 34 | +```css |
| 35 | +// light mode is default |
| 36 | +body { |
| 37 | + background: white; |
| 38 | +} |
| 39 | + |
| 40 | +h1 { |
| 41 | + color: black; |
| 42 | +} |
| 43 | + |
| 44 | +// dark mode has a more specific selector, so will override the light mode |
| 45 | +body.dark { |
| 46 | + background: black; |
| 47 | +} |
| 48 | + |
| 49 | +.dark h1 { |
| 50 | + color: white; |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +## How to define the dark CSS styles |
| 55 | + |
| 56 | +My site has a single `styles.css` that defines the site's theme, along with a `reset.css` for resetting browser defaults to 0 values and `syntax_highlight.css` for my syntax block styling. All these live in a `styles` folder, and my build process merges them all together and minifies them. |
| 57 | + |
| 58 | +So, my plan is to copy/paste my `styles.css` to `styles_dark.css`, and remove any properties that aren't about colours. It was a slightly tedious process, but only took me 15 minutes to isolate all the colour values in my CSS. Now during development (before I've created the JavaScript), I manually added the `dark` class to my body element, and ran the site in development mode. |
| 59 | + |
| 60 | +Now I've got a file with just the colours, I prepend the `.dark` class selector to the beginning of each of them. Because it's a standard CSS file, I select all instances of `{` using VS Code `CTRL/CMD + D`, then hit the `Home` key to take me to the beginning of each line, and smack in `.dark` - now all my `styles_dark.css` selectors are prefixed with the correct selector - neat! |
| 61 | + |
| 62 | +With my site running in development mode and the body being hard-coded to be `class="dark"`, I just modified all the values until I was happy with the darker theme. |
| 63 | + |
| 64 | +If this sounds like a lot of manual steps, it is! But only because my site is very, _very_ simplistic in its implementation for maximum speed/ease of maintenance. |
| 65 | + |
| 66 | +## How to flip between light and dark mode |
| 67 | + |
| 68 | +### Simple functionality |
| 69 | + |
| 70 | +Now I've decided on adding the `dark` class to my `<body>` tag, I can write some JavaScript to control this functionality. To begin with, I just added a button to the top of my home page (who cares about styling right now), and some simple JavaScript: |
| 71 | + |
| 72 | +```html |
| 73 | +<!-- HTML --> |
| 74 | + |
| 75 | +<!-- At the top of my home page --> |
| 76 | +<button onclick="switchMode()">Switch dark/light mode</button> |
| 77 | + |
| 78 | +<!-- Just before the closing body tag --> |
| 79 | +<script> |
| 80 | + var darkMode = false; |
| 81 | + function switchMode() { |
| 82 | + if (darkMode) { |
| 83 | + darkMode = false; |
| 84 | + document.body.classList.remove('dark'); |
| 85 | + } else { |
| 86 | + darkMode = true; |
| 87 | + document.body.classList.add('dark'); |
| 88 | + } |
| 89 | + } |
| 90 | +</script> |
| 91 | +``` |
| 92 | + |
| 93 | +Hooray! Now the site switches between light and dark mode as expected. |
| 94 | + |
| 95 | +### Remembering preference |
| 96 | + |
| 97 | +Now we're switching between modes, but it doesn't remember my last mode on page refresh! Let's use local storage to remember our value: |
| 98 | + |
| 99 | +```javascript |
| 100 | +// retrieve from local storage |
| 101 | +var darkMode = window.localStorage.getItem('darkMode') || 'false'; |
| 102 | +if (darkMode === 'true') { |
| 103 | + document.body.classList.add('dark'); |
| 104 | +} |
| 105 | + |
| 106 | +// enable switching functionality |
| 107 | +function switchMode() { |
| 108 | + if (darkMode === 'true') { |
| 109 | + darkMode = 'false'; |
| 110 | + document.body.classList.remove('dark'); |
| 111 | + } else { |
| 112 | + darkMode = 'true'; |
| 113 | + document.body.classList.add('dark'); |
| 114 | + } |
| 115 | + window.localStorage.setItem('darkMode', darkMode); |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +Note that we have to use string versions of `true` and `false`, because local storage _always_ stores things as strings. Now our site remembers our preference, defaulting to light mode. |
| 120 | + |
| 121 | +### Honouring system preference |
| 122 | + |
| 123 | +This is great, but if the site is being viewed on a browser/machine with dark mode enabled system-wide, we should honour that too by default: |
| 124 | + |
| 125 | +```javascript |
| 126 | +// detect browser setup |
| 127 | +if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && !window.localStorage.getItem('darkMode')) { |
| 128 | + window.localStorage.setItem('darkMode', 'true'); |
| 129 | +} |
| 130 | + |
| 131 | +// retrieve from local storage |
| 132 | +var darkMode = window.localStorage.getItem('darkMode') || 'false'; |
| 133 | +if (darkMode === 'true') { |
| 134 | + document.body.classList.add('dark'); |
| 135 | +} |
| 136 | + |
| 137 | +// enable switching functionality |
| 138 | +function switchMode() { |
| 139 | + if (darkMode === 'true') { |
| 140 | + darkMode = 'false'; |
| 141 | + document.body.classList.remove('dark'); |
| 142 | + } else { |
| 143 | + darkMode = 'true'; |
| 144 | + document.body.classList.add('dark'); |
| 145 | + } |
| 146 | + window.localStorage.setItem('darkMode', darkMode); |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +There are some caveats to this method (it's supported only in modern browsers), but it's good enough to cover 95% of users of the web, and I would suspect almost 100% of viewers of my site (due to the target audience). Again, it will default to light mode if it can't use `window.matchMedia`, so no harm no foul. |
| 151 | + |
| 152 | +## Pretty SVG button |
| 153 | + |
| 154 | +Finally, I updated the styling of my boring-standard button to be something snazzier, using a nice SVG of a filled or hollow sun: |
| 155 | + |
| 156 | +```html |
| 157 | +<section class="darkmode"> |
| 158 | + <button onclick="switchMode()"> |
| 159 | + <svg class="lightSun" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 48 48"> |
| 160 | + <g id="Layer_2" data-name="Layer 2"> |
| 161 | + <g id="invisible_box" data-name="invisible box"> |
| 162 | + <rect width="48" height="48" fill="none" /> |
| 163 | + </g> |
| 164 | + <g id="Q3_icons" data-name="Q3 icons"> |
| 165 | + <g> |
| 166 | + <path d="M24,10a2,2,0,0,0,2-2V4a2,2,0,0,0-4,0V8A2,2,0,0,0,24,10Z" /> |
| 167 | + <path d="M24,38a2,2,0,0,0-2,2v4a2,2,0,0,0,4,0V40A2,2,0,0,0,24,38Z" /> |
| 168 | + <path d="M36.7,14.1l2.9-2.8a2.3,2.3,0,0,0,0-2.9,2.3,2.3,0,0,0-2.9,0l-2.8,2.9a2,2,0,1,0,2.8,2.8Z" /> |
| 169 | + <path d="M11.3,33.9,8.4,36.7a2.3,2.3,0,0,0,0,2.9,2.3,2.3,0,0,0,2.9,0l2.8-2.9a2,2,0,1,0-2.8-2.8Z" /> |
| 170 | + <path d="M44,22H40a2,2,0,0,0,0,4h4a2,2,0,0,0,0-4Z" /> |
| 171 | + <path d="M10,24a2,2,0,0,0-2-2H4a2,2,0,0,0,0,4H8A2,2,0,0,0,10,24Z" /> |
| 172 | + <path d="M36.7,33.9a2,2,0,1,0-2.8,2.8l2.8,2.9a2.1,2.1,0,1,0,2.9-2.9Z" /> |
| 173 | + <path d="M11.3,14.1a2,2,0,0,0,2.8-2.8L11.3,8.4a2.3,2.3,0,0,0-2.9,0,2.3,2.3,0,0,0,0,2.9Z" /> |
| 174 | + <path d="M24,14A10,10,0,1,0,34,24,10,10,0,0,0,24,14Zm0,16a6,6,0,1,1,6-6A6,6,0,0,1,24,30Z" /> |
| 175 | + </g> |
| 176 | + </g> |
| 177 | + </g> |
| 178 | + </svg> |
| 179 | + <svg class="darkSun" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 48 48"> |
| 180 | + <g id="Layer_2" data-name="Layer 2"> |
| 181 | + <g id="invisible_box" data-name="invisible box"> |
| 182 | + <rect width="48" height="48" fill="none" /> |
| 183 | + </g> |
| 184 | + <g id="Q3_icons" data-name="Q3 icons"> |
| 185 | + <g> |
| 186 | + <path d="M24,10a2,2,0,0,0,2-2V4a2,2,0,0,0-4,0V8A2,2,0,0,0,24,10Z" /> |
| 187 | + <path d="M24,38a2,2,0,0,0-2,2v4a2,2,0,0,0,4,0V40A2,2,0,0,0,24,38Z" /> |
| 188 | + <path d="M36.7,14.1l2.9-2.8a2.3,2.3,0,0,0,0-2.9,2.3,2.3,0,0,0-2.9,0l-2.8,2.9a2,2,0,1,0,2.8,2.8Z" /> |
| 189 | + <path d="M11.3,33.9,8.4,36.7a2.3,2.3,0,0,0,0,2.9,2.3,2.3,0,0,0,2.9,0l2.8-2.9a2,2,0,1,0-2.8-2.8Z" /> |
| 190 | + <path d="M44,22H40a2,2,0,0,0,0,4h4a2,2,0,0,0,0-4Z" /> |
| 191 | + <path d="M10,24a2,2,0,0,0-2-2H4a2,2,0,0,0,0,4H8A2,2,0,0,0,10,24Z" /> |
| 192 | + <path d="M36.7,33.9a2,2,0,1,0-2.8,2.8l2.8,2.9a2.1,2.1,0,1,0,2.9-2.9Z" /> |
| 193 | + <path d="M11.3,14.1a2,2,0,0,0,2.8-2.8L11.3,8.4a2.3,2.3,0,0,0-2.9,0,2.3,2.3,0,0,0,0,2.9Z" /> |
| 194 | + <path d="M24,14A10,10,0,1,0,34,24,10,10,0,0,0,24,14Z" /> |
| 195 | + </g> |
| 196 | + </g> |
| 197 | + </g> |
| 198 | + </svg> |
| 199 | + </button> |
| 200 | +</section> |
| 201 | +``` |
| 202 | + |
| 203 | +And I use CSS to show or hide the appropriate SVG: |
| 204 | + |
| 205 | +```css |
| 206 | +// default to showing darkSun (ie, light mode) |
| 207 | +.lightSun { |
| 208 | + display: none; |
| 209 | +} |
| 210 | + |
| 211 | +// show lightSun and hide darkSun in dark mode |
| 212 | +.dark .lightSun { |
| 213 | + display: block; |
| 214 | + fill: #fff; |
| 215 | +} |
| 216 | +.dark .darkSun { |
| 217 | + display: none; |
| 218 | +} |
| 219 | +``` |
| 220 | + |
| 221 | +Now I just add this code to all my pages, popping the JavaScript in my `footer.html` partial, and the SVG button to my `header.html` partial - now it's on every page! |
| 222 | + |
| 223 | +## This isn't perfect |
| 224 | + |
| 225 | +This is by no means a perfect implementation - the biggest issue is that there's a "flash of unstyled content" when the page loads and it runs the JavaScript to detect if you have dark mode on, and _then_ applies the `dark` class to the `<body>` element. However, for this _incredibly basic site_ it happens in ~2ms, so I think it's a trade-off worth making for the utter simplicity of the implementation. |
| 226 | + |
| 227 | +You can check out all the eventual code for this in this website's repository: |
| 228 | + - [darkmode.html partial](https://github.com/ripixel/ripixel-website/blob/master/partials/darkmode.html) (for the switching button, included on every page) |
| 229 | + - [footer.html partial](https://github.com/ripixel/ripixel-website/blob/master/partials/footer.html#L30) (for the switching JavaScript |
| 230 | + - [styles_dark.css](https://github.com/ripixel/ripixel-website/blob/master/assets/styles/styles_dark.css) (the dark mode specific styles) |
| 231 | + |
| 232 | +Go click that sun in the top-right hand corner of the site and let me know what you think! |
0 commit comments