Progressively-enhanced dark mode

Recently, I added a dark mode setting to my website. Dark mode is a color theme feature that’s pretty common these days. Letting users adjust the color of content so it’s easier to see is a nice usability improvement.
Many developers I follow have written great articles about their implementations, which are full of valuable insight. To name a few: Hidde de Vries, Sarah Fossheim, and Josh Comeau all have smart approaches with valuable takeaways.
However, I decided to take a slightly different approach than a lot of the solutions I’d seen – a progressively-enhanced version that works with or without JavaScript (JS).
Progressive Enhancement #
I’m a fan of progressive enhancement – starting with a simple, functional base that’s enhanced with newer, more complex features if supported. In this case, I’m referring to the availability of JS.
JS is a great tool for adding rich and interactive web features. Unfortunately, many developers lean too heavily on it, resulting in broken sites when it’s not available.
It’s estimated that between .2-1% of worldwide users browse the web without JS. That might seem minuscule, but let’s put that into perspective:
Note:
Analytics show my site had about 5,000 unique visitors in 2022, so .2-1% of that is 20-50 people. If 50 people told me I prevented them from reading an article they were interested in, I’d be mortified.
Now instead of my site imagine it’s something important like a health care portal with infinitely more users – a lot of people could be blocked from accessing essential services.
JS can fail to load #
Sometimes network requests time out, or an ad-blocker or corporate firewall prevents JS from loading. You could also push a small update with a typo that breaks functionality. Sites should still work for people when these things happen.
JS can be intentionally disabled #
Many sites use a ton of JS, forcing users to download large files with thousands of lines of code. And connections can sometimes be slow or unstable (like airplane Wi-Fi), so disabling JS may be the only way to get pages to load. Others use limited or metered data plans that go further by blocking scripts.
Some privacy-focused users want to be less vulnerable to monitoring, whether by tech giants like Meta and Google, or they live in countries with nefarious regimes.
Lastly, many can’t upgrade devices frequently, while others use shared library or school machines. These might not run modern JS or may be configured to block scripts to run smoother.
Blocking JS will become easier #
Many devices already have “low data” modes that limit how some apps can use the network. It’s only a matter of time before this extends to web browsers where it could block the loading of things like JS.
The upcoming CSS media query, prefers-reduced-data
, will let developers detect these settings. So users will soon be able to ask for leaner, simpler pages. If we truly care about our users, we should build sites to honor these preferences.
Regardless of the reason, it’s important to me that my site works both with and without JS, and any dark mode function I add is no different.
A JavaScript-less solution #
CSS functionality #
I started with looking at the options available through CSS.
Any color theme picker needs a set of CSS styles to give the site its light and dark appearances. Using the CSS prefers-color-scheme
media query, I can detect if a user has set a theme preference on their device, and apply certain styles if so.
I initially built my site with a light color scheme. So with that as the default, I can now use the prefers-color-scheme
query to apply dark styles if it detects the dark value. Here’s a simplified version of that:
Basic dark mode in CSS
cssbody { background: white; color: black; } @media (prefers-color-scheme: dark) { body { background: black; color: white; } }
Now if a user’s system theme changes, the site will change along with it.
While this is a nice first step, I see some issues with this approach:
- Users might not know about the color theme settings in their device’s operating system (OS). If they want to view a site in a different theme they’ll be stuck.
- Changing an OS setting just to view one website differently is overkill. If a user prefers a light system theme but wants to view a website in a dark theme they have to change their system’s setting then flip it back once they’re done. That’s a hassle.
- The user can’t save changes to their system’s theme for specific websites. If a user switches their system theme to view a site easier, they may have to do so again the next time they visit.
In order to solve these limitations I’m going to need more than a CSS-only approach.
Finding the right input #
It’s clear now that the site needs to both adapt to the system theme, and allow users to override that and set their own. I’ll need to create a mechanism that builds off of the CSS functionality.
The problem with toggles #
Most implementations I see use a toggle as an input mechanism. But toggles only have two states – in this case “light” and “dark”. Users can set a site’s theme but have no way to “unset” their selection if they ever want to. They’re always stuck in one override or the other.
So I’ll also need an “auto” mode that removes any overrides. A toggle isn’t going to work.
“Auto”, “light”, and “dark” makes three options. I may also add additional color themes in the future, so the input needs to be flexible enough for me to add more later.
Radio inputs #
I considered a few different inputs but ultimately went with radio buttons (<input type=”radio” />
). I liked that users can see the options upfront, they’re forgiving with mistakes, and it’s easy to add more choices in the future

Each radio input
is paired with a label
and given a name
of “theme”, which is how the submitted data will get located on the server. Then I wrapped everything in a fieldset
with a legend
to give it one semantic grouping and name:
HTML markup of inputs
html<fieldset> <legend>Color theme:</legend> <input type="radio" id="theme--auto" name="theme" value="auto" /> <label for="theme--auto">Auto</label> <input type="radio" id="theme--light" name="theme" value="light" /> <label for="theme--light">Light</label> <input type="radio" id="theme--dark" name="theme" value="dark" /> <label for="theme--dark">Dark</label> </fieldset>
An HTML form #
With no JS available I’ll need an old-school HTML form
that submits the data to a server.
My site’s back-end compiles pages server-side. So if a user submits the form, the server can grab and use that data as it builds the next page.
HTML form
elements have an action
attribute to specify where requests are sent. Here I give it the "/"
value, meaning the current page. This results in the page essentially refreshing itself, only now with access to the data.
Using the method
attribute, I indicate how to send the request. I opted for the GET
method (vs. POST
), meaning it appends data to the URL of the new page as key/value parameters, like this: https://darins.page?theme=dark
.
Note:
The method
used isn’t too important as long as the data gets to the server. But using GET
gives me the option to set a theme via the URL. If I ever needed to share a site link with a particular theme pre-set, I could do so like this: https://darins.page/articles/article-name?theme=dark
Here’s the form
markup with the fieldset
and a submit button
added. Note the type=”submit”
is needed to trigger the form’s action
.
HTML markup of form and button
html<form action="/" method="GET"> <fieldset>...</fieldset> <button type="submit">GO</button> </form>
Processing the form
#
Craft, my site’s CMS, uses the PHP templating engine TWIG and its own API functions to markup dynamic HTML pages. Don’t fret if your environment is different, the logic is the key part here.
Here’s the relevant part of my page template:
TWIG template demonstrating retrieving and storing a theme
twig<!-- create 'theme' var with default 'auto' value --> {% set theme = 'auto' %} <!-- get any submitted form data --> {% set formTheme = craft.app.request.getParam('theme') %} <!-- if 'formTheme' has value, form was submitted --> {% if formTheme is not null %} <!-- update 'theme' var with form value --> {% set theme = formTheme %} <!-- store form value in session var --> {% do craft.app.session.set("sessionTheme",theme) %} <!-- if form wasn't submitted check if session var exists --> {% elseif craft.app.session.get("sessionTheme")|length %} <!-- update 'theme' var with value of session var --> {% set theme = craft.app.session.get("sessionTheme") %} {% endif %} <!DOCTYPE html> <!-- write 'theme' to html tag --> <html data-theme="{{ theme }}">
I first use Craft’s request.getParam()
function to grab any GET
data with the name “theme” and check if it has a value – if so, it means the form was submitted. I then store the data on the server in a “session variable” using session.set()
.
Note:
A session is a temporary handshake between a browser and site’s server. This is how I’ll “save” the user’s theme without JS. The limitation is if a user revisits the site after the session expires, any set preference will be lost. But that’s a trade-off of not using JS.
If the form wasn’t submitted, I check if a session variable already exists with session.get()
. If yes, it means the user set a preference already and then navigated here. So I grab that value.
If neither of those results in a theme value, it means the user either hasn’t set a theme or the session has expired, so the default of “auto” remains.
I write the value to the data-theme
attribute on the html
tag. Based on the user’s submission, it will result in this:
html element with data attribute
html<!-- if user submits "auto", --> <!-- or no session or form data is detected --> <html data-theme="auto"> <!-- if user submits "light" --> <html data-theme="light"> <!-- if user submits "dark" --> <html data-theme="dark">
Now the form works, data is incorporated into the page template, and a session stores any preference for the remainder of the visit.
Pre-selecting the current radio #
The form is functional, but the radio buttons still load empty. Since the page template checks for any active theme preferences, I can use that to pre-select the current theme’s input
.
Based on the value of theme
, I add the checked
attribute to the corresponding input
. Here’s the “auto” radio as an example:
Dynamically adding checked attribute to radio inputs
twig<input id="theme--auto" type="radio" name="theme" value="auto" {% if theme == 'auto' %} checked="checked" {% endif %} >
Now when the page loads the radio that matches the current theme will be selected by default.
Revisiting CSS #
The final piece is to modify the CSS to honor the data-theme
attribute. My initial CSS only responded to changes in the system’s color mode, so I need to adjust it to let data-theme
override that.
CSS dark mode with data attribute selectors
cssbody { background: white; color: black; } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { body { background: black; color: white; } } } [data-theme="dark"] { body { background: black; color: white; } }
Inside the prefers-color-scheme
media query I now apply the dark mode styles only if the root html
element does not have the data-theme=”light”
attribute. This allows users to override their system’s dark mode with my site’s light theme.
I also add a new selector solely for data-theme=”dark”
that acts as the dark mode override against a system light theme.
Unfortunately, this results in duplicated dark styles in my stylesheet – a currently unavoidable issue. Bramus Van Damme discusses this code duplication in his dark mode study and touches on ways it could be solved down the road.
I can at least avoid writing and maintaining duplicate CSS by using SCSS imports, even if it does compile into duplicated code:
SCSS importing dark mode file
scss/* main.scss */ body { background: white; color: black; } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { @import 'dark.scss'; } } [data-theme="dark"] { @import 'dark.scss'; }
Dark mode SCSS being imported
scss/* dark.scss */ body { background: black; color: white; }
The completed form #
Now the color theme picker is complete and functional. If a user visits my site with JS disabled they can still change the theme.
Here’s a screen-recording of it in action:
Video transcript
[Video begins. There is no audio track.]
The video is a screen recording of Darin Senneff's home page on a laptop web browser. A color theme form sits below the navigation in the website's header.
The form has a labeled group "Color theme:" in front of three radio buttons. The radio buttons are labeled "Auto", "Light", and "Dark". After the radio buttons is a submit button labeled "Go". The page has a light color theme, which is reflected in the "Light" radio button of the form being selected.
The following actions then occur:
- The mouse cursor selects "Dark", then the submit button is clicked. The page refreshes in a dark color theme with the "Dark" radio selected by default.
- The cursor selects the "Auto" radio button, then clicks the submit button. The page refreshes, still in a dark color theme with the "Auto" radio selected by default.
- The cursor selects the "Light" radio button, then clicks the submit button. The page refreshes, now in a light color theme with the "Light" radio selected by default.
[Video ends.]
A JavaScript-enhanced solution #
The theme picker works nicely, and I could leave it as-is. But there are a few enhancements I can think of for JS users:
- Changing themes doesn’t require a page refresh. JS can apply a color theme without refreshing the page.
- User preferences can be saved reliably. Unlike a server session, JS can save the user’s theme for future visits.
- Users can preview changes and cancel if they don’t like it.
- More flexibility for future additions. JS can allow the user to access color themes alongside related settings I add.
Pivoting from the old form #
I first wrap the existing form in a noscript
tag. The noscript
element only renders its contents when JS isn’t available.
Form wrapped in noscript and a new button
html<noscript> <form action="/" method="GET">...</form> </noscript> <button id="themeButton" hidden type="button">Color theme</button>
Below it I add a new button
with a hidden
attribute, which hides elements by default both onscreen and from assistive technology (AT).
Adding hidden markup for progressive-enhancement purposes does add size to the page, which you’ll have to consider. In this case it’s one element, so the effect is miniscule.
So no-JS users will see the form but not the button. But when JS is available the form isn’t rendered while the button is revealed by removing the hidden
attribute:
Removing the hidden attribute
jsconst themeButton = document.querySelector('#themeButton'); themeButton.removeAttribute('hidden');
With a bit of visual styling, the button looks like this:

Adding a dialog box #
The JS approach hides the low-fi form and displays a button
. But what does that button
do?
Some enhancements I wanted were to let users preview theme changes, while also providing a place to access more settings I add in the future. I decided that I can do that with a dialog box.
Why dialog? #
A dialog (or modal) is a window that opens over the page, blocking the underlying interface. Developers often overuse dialogs, but I think it makes sense here.
I may add more themes or other customization tools, which could fill up the header with too many controls. Placing everything into a dialog lets me keep the header clean while establishing one location where users can adjust preferences.
The dialog also lets users preview a change and either confirm or cancel if they wish. Eric Eggert also uses a dialog in his approach, mentioning this as a benefit.
Dialog markup #
HTML has a native dialog
element but it has support gaps still, which my analytics show affects some of my visitors (Safari pre-15.4). So I used the great a11y-dialog by Kitty Giraudel, an accessible dialog plugin with good support.
The markup a11y-dialog uses looks like this, which I inject with JS just inside the closing body
tag:
Dialog markup
html<div id="themeDialog" role="dialog" aria-hidden="true" aria-modal="true" tabindex="-1" aria-labelledby="themeDialog--title"> <div data-a11y-dialog-hide class="themeDialog--backdrop"></div> <div role="document"> <button data-a11y-dialog-hide type="button">Close dialog</button> <h1 id="themeDialog--title">Color theme</h1> <form> <fieldset> <legend>Select a theme</legend> <input id="themeDialog--auto" type="radio" name="theme" value="auto" checked> <label for="themeDialog--auto">Auto: Use your device’s system setting.</label> <input id="themeDialog--light" type="radio" name="theme" value="light"> <label for="themeDialog--light">Light: Use the light theme.</label> <input id="themeDialog--dark" type="radio" name="theme" value="dark"> <label for="themeDialog--dark">Dark: Use the dark theme.</label> </fieldset> <button data-a11y-dialog-hide type="button">Cancel</button> <button data-a11y-dialog-hide type="submit">Save</button> </form> </div> </div>
The dialog has aform
wrapping a fieldset
with radio input
s, similar to earlier. There’s also a close, cancel, and save button
. Note that there’s a type="submit"
on the save button so users can still trigger that with the enter key.
After styling, it looks like this:

Storing and retrieving themes #
localStorage is an easy way to store long-term data in the browser with JS. Most approaches to color themes use it and I will too. The ability to recall a user’s theme on future visits is another JS enhancement.
My only difference to other approaches is this: if localStorage is empty, I fall back to use the theme that is stored in a session and added to the html
data-theme
attribute by the server.
This covers the rare case where a user starts with JS disabled and then enables it for whatever reason. Their theme setting, if one was set, will still be passed along.
Here’s the JS logic:
Logic for getting and storing a theme
js// create 'currentTheme' var with default of 'auto' let currentTheme = 'auto'; // check if browser storage has theme saved if(localStorage.getItem('theme')) { // set 'currentTheme' to browser value currentTheme = localStorage.getItem('theme'); // update html data attr document.documentElement.dataset.theme = currentTheme; // else if server added theme to html tag } else if(document.documentElement.dataset.theme) { // set 'currentTheme' to server val currentTheme = document.documentElement.dataset.theme; // store server value in browser storage localStorage.setItem('theme',currentTheme); }
I start by defining a currentTheme
variable with a default value of “auto”.
I then check browser storage for a theme via localStorage.getItem()
. If there is, I update both currentTheme
and the data-theme
attribute of the html
element with that.
If nothing was in browser storage I grab the existing data-theme
value off of the html
tag and update currentTheme
with that. I then store that in the browser with localStorage.setItem()
.
Showing and hiding the dialog #
Now that the dialog is marked up and I’m getting any stored themes, I need to make the dialog functional. I first initialize the a11y-dialog instance by passing in a reference to the dialog container.
Initializing a11y dialog
js// get reference to dialog container const themeDialog = document.querySelector('#themeDialog'); // create a11y-dialog instance const a11yDialog = new A11yDialog(themeDialog);
Once initialized, a11y-dialog conveniently finds any elements with data-a11y-dialog-show|hide
attributes and uses those as triggers to open and close the dialog. The -hide
version was put on the dialog’s backdrop and close, cancel, and save buttons above, while -show
goes on the button that opens the dialog:
Color theme button with data attribute
html<button data-a11y-dialog-show="themeDialog" id="themeButton" type="button" hidden>Color theme</button>
When those trigger elements are used, a11y-dialog sets the aria-hidden
value of the dialog to “true” or “false”. When “true”, this CSS hides the dialog:
CSS hiding dialog with aria-hidden
css#themeDialog[aria-hidden="true"] { display: none; }
User actions #
Changing themes #
Unlike the no-JS form, I want users to preview changes as radios are selected. To do this I listen for the change
event and pass the value of the selected radio to a changeTheme()
function I created.
Change event listener
jsthemeDialog.addEventListener('change',function(e) { // check that radio input triggered event if(e.target.nodeName === 'INPUT') { // set color theme temporarily changeTheme(e.target.value); } });
The changeTheme()
function takes that value and updates the data-theme
attribute on the html
element. It also stores that same theme in the tempTheme
variable.
changeTheme function
jsfunction changeTheme(theme) { // set html data attr to selected theme document.documentElement.dataset.colorTheme = theme; // save theme value in tempTheme var tempTheme = theme; }
Now when a user changes a radio button in the dialog, the page’s theme changes.
Video transcript
[Video begins. There is no audio track.]
The video is a screen recording of Darin Senneff's home page on a laptop web browser. A dialog window is open in the foreground with the rest of the page dimmed behind it. The page and dialog window appear in a light color scheme.
The dialog is titled "Color Theme". The content starts with the text "Select a theme. Your color preference is stored on your device, I do not save or collect any data." Below the text are three radio buttons, "Auto", "Light", and "Dark". Below the form are two buttons, "Cancel" and "Save".
The following actions then occur:
- The mouse cursor selects "Dark". The page immediately changes to a dark color theme.
- The cursor selects "Auto". The page remains in a dark color theme.
- The cursor selects "Light". The page immediately changes to a light color theme.
[Video ends.]
Once a selection is made, the user has two options: cancel or save.
Cancelling a selection #
Cancelling needs to close the dialog and revert the page theme back to what it was before the dialog opened.
Users can cancel several ways: the “close” button; the “cancel” button; the backdrop behind the dialog; or their keyboard’s esc key. As I mentioned, a11y-dialog handles the closing via the data-a11y-dialog-hide
attribute, while it adds the esc key functionality automatically.
When a close happens, a11y-dialog dispatches a hide
event, which I listen for using its on()
method:
a11y-dialog onhide event listener
jsa11yDialog.on('hide', function (element, event) { // prevent default form actions event.preventDefault(); // check if esc key or any element aside from submit button triggered this if(event.type === 'keydown' || event.currentTarget.getAttribute('type') !== 'submit') { cancelTheme(); } else { saveTheme(tempTheme); } // clear 'tempTheme' var tempTheme = null; });
Inside, I prevent any default form behavior with event.preventDefault()
. Then I check if the hide was due to a cancel or a save.
This logic just checks if the close occurred due to a keydown
event (the esc key was used) or from an element that’s not the save button (doesn’t have type=“submit”
), in which case it was a cancel. So we fire the cancelTheme()
method.
Otherwise the save button was used and the saveTheme()
method is called, which I’ll get into shortly.
At the end of the event handler the tempTheme
variable is cleared.
Looking at the cancelTheme()
function:
cancelTheme function
jsfunction cancelTheme() { if(!tempTheme) return; // get radio button for previous value and set it back to checked themeDialog.querySelector(`#themeDialog--${currentTheme}`).checked = true; // update html data attr to previous value document.documentElement.dataset.colorTheme = currentTheme; }
I start by checking if tempTheme
has a value. If not, it
means that the user didn’t change themes while the dialog was open, so I
can just quit the function. If it does, the user did change themes so I
need to revert.
Using currentTheme
, which always stores the last saved theme, I add the checked
attribute to the correct radio input
and update the page’s data-theme
attribute.
Here's a video of the cancel functionality in action:
Video transcript
[Video begins. There is no audio track.]
The video is a screen recording of Darin Senneff's home page on a laptop web browser. A dialog window is open in the foreground with the rest of the page dimmed behind it. The page and dialog window appear in a light color scheme.
The dialog is titled "Color Theme". There's a close button in the upper-right corner displaying an X icon. The content starts with the text
"Select a theme. Your color preference is stored on your device, I do
not save or collect any data." Below the text are three radio buttons,
"Auto", "Light", and "Dark". Below the form are two buttons, "Cancel"
and "Save".
The following actions then occur:
- The mouse cursor selects "Dark". The page immediately changes to a dark color theme.
- The cursor then clicks on the backdrop layered between the dialog and the page in the background. The dialog closes and the page reverts back to the light theme.
- The cursor clicks on the "color theme" button, opening the dialog.
- The cursor selects "Auto". The page immediately changes to a dark color theme.
- The cursor then clicks on the "Cancel" button. The dialog closes and the page reverts back to the light theme.
- The cursor clicks on the "color theme" button, opening the dialog.
- The cursor selects "Dark". The page immediately changes to a dark color theme.
- The cursor then clicks on the "Close" button with the X icon. The dialog closes and the page reverts back to the light theme.
- The cursor clicks the "color theme" button, opening the dialog.
[Video ends.]
Saving a selection #
If the user uses the save button instead, the saveTheme()
function fires:
saveTheme function
jsfunction saveTheme(theme) { // store theme in browser localStorage.setItem('theme', theme); // send data to server to update session var sendTheme(theme); // set currentTheme to selected theme currentTheme = theme; }
A theme is passed in as a parameter from the hide
event. I first update browser storage with localStorage.setItem()
. Next I fire another function, sendTheme()
, which I’ll get to in a moment. Last, I update the currentTheme
variable with the new theme value.
Now when the user presses the save button the dialog closes and their selection becomes the active theme.
The sendTheme()
function essentially uses the Fetch API to send a “behind the scenes” form submission, which updates the server session theme. This covers the edge case where a user begins with JS, sets a theme, and later disables JS. Rare, but I like the idea of it still working.
sendTheme function
jsfunction sendTheme(theme) { // check if fetch is supported if(!window.fetch) return; // send fetch request fetch(`/?theme=${theme}`) .then(function(response) { // do something with response console.log('Theme send response:',response); }); }
I begin by checking if fetch
is supported. If so, I send a request using the fetch()
method to a string-concatenated path containing the selected theme as a
URL parameter. So if a user chooses a “light” theme, the URL of the
fetch request will be “/?theme=light”
. That’s the same location the no-JS form sent requests, so I’m repurposing that infrastructure from earlier here.
Now the JS functionality is complete. When a user saves a selection, their theme is saved in both browser localStorage and on the server as a session variable.
Here's how the save functionality looks:
Video transcript
[Video begins. There is no audio track.]
The video is a screen recording of Darin Senneff's home page on a laptop web browser. A dialog window is open in the foreground with the rest of the page dimmed behind it. The page and dialog window appear in a light color scheme.
The dialog is titled "Color Theme". There's a close button in the
upper-right corner displaying an X icon. The content starts with the
text
"Select a theme. Your color preference is stored on your device, I do
not save or collect any data." Below the text are three radio buttons,
"Auto", "Light", and "Dark". Below the form are two buttons, "Cancel"
and "Save".
The following actions then occur:
- The mouse cursor selects "Dark". The page immediately changes to a dark color theme.
- The
cursor then clicks on the "Cancel" button. The dialog closes and the page reverts back to the light theme.
- The cursor clicks on the "color theme" button, opening the dialog.
- The cursor selects "Dark". The page immediately changes to a dark color theme.
- The cursor then clicks on the "Save" button. The dialog closes and the page remains in the dark color theme.
- The cursor clicks on the "color theme" button, opening the dialog.
[Video ends.]
Conclusion #
Rolling out a dark mode feature on a site was a new experience for me. I learned a ton from reading the approaches of others, and was able to combine this with my own knowledge to craft a solution that works for my needs.
It was important to me that the feature works for both JS and no-JS users. I’m certainly far from the first, but I haven’t seen many articles that approach the feature from this angle. So I hope that my process was helpful or interesting to you in some way.
Like many developers, I use my own site as a laboratory of sorts to implement and test new features. If you experience any issues with using the feature, please let me know so I can learn why. And as always, if you have comments or questions shoot me and email or hit me up on Mastodon.
Resources #
Here’s a list of resources and terms I mentioned or are relevant to this article:
Dark mode implementations #
- How I built a dark mode toggle by Hidde de Vries – Hidde details his approach to a dark mode toggle button.
- Building an accessible theme picker with HTML, CSS and JavaScript by Sarah Fossheim – Sarah discusses their approach to an accessible color theme picker.
- The quest for the perfect dark mode by Josh Comeau – Josh details his approach to a dark mode feature in a React + Gatsby environment.
- Dark Mode Toggles Should be a Browser Feature by Bramus Van Damme – Bramus overviews a dark mode 101 approach and discusses some pros and cons.
- Welcome to the dark side by Eric Eggert – Eric breaks down his dark mode approach that features a dialog box.
- AdrianRoselli.com by Adrian Roselli – Adrian’s website color theme picker works with and without JS by progressively enhancing links containing URL parameters into JS-powered buttons.
Code and technology #
- prefers-reduced-data on MDN Web Docs
- prefers-color-scheme on MDN Web Docs
- Craft CMS – A PHP-based content management system
- TWIG – A PHP template engine
- PHP Sessions on W3Schools
noscript
: The Noscript element on MDN Web Docshidden
on MDN Web Docsdialog
: The Dialog element on MDN Web Docs- a11y Dialog by Kitty Giraudel – a lightweight script to create accessible dialog windows.
- Window.localStorage on MDN Web Docs