Setting And Persisting Color Scheme Preferences With CSS And A\u00a0\u201cTouch\u201d\u00a0Of JavaScript<\/h1>\nHenry Bley-Vroman<\/address>\n 2024-03-25T12:00:00+00:00
\n 2024-10-15T23:05:45+00:00
\n <\/header>\n
Many modern websites give users the power to set a site-specific color scheme preference. A basic implementation is straightforward with JavaScript: listen for when a user changes a checkbox or clicks a button, toggle a class (or attribute) on the <body><\/code> element in response, and write the styles for that class to override design with a different color scheme.<\/p>\nCSS\u2019s new :has()<\/code> pseudo-class<\/a>, supported by major browsers since December 2023, opens many doors for front-end developers. I\u2019m especially excited about leveraging it to modify UI in response to user interaction without JavaScript<\/em>. Where previously we have used JavaScript to toggle classes or attributes (or to set styles directly), we can now pair :has()<\/code> selectors with HTML\u2019s native interactive elements.<\/p>\nSupporting a color scheme preference, like \u201cDark Mode,\u201d is a great use case. We can use a <select><\/code> element anywhere that toggles color schemes based on the selected <option><\/code> — no JavaScript needed, save for a sprinkle to save the user\u2019s choice, which we\u2019ll get to further in.<\/p>\nRespecting System Preferences<\/h2>\n
First, we\u2019ll support a user\u2019s system-wide color scheme preferences by adopting a \u201cLight Mode\u201d-first approach. In other words, we start with a light color scheme by default and swap it out for a dark color scheme for users who prefer it.<\/p>\n
The prefers-color-scheme<\/code><\/a> media feature detects the user\u2019s system preference. Wrap \u201cdark\u201d styles in a prefers-color-scheme: dark<\/code> media query.<\/p>\nselector {\n \/* light styles *\/\n\n @media (prefers-color-scheme: dark) {\n \/* dark styles *\/\n }\n}\n<\/code><\/pre>\nNext, set the color-scheme<\/code><\/a> property to match the preferred color scheme. Setting color-scheme: dark<\/code> switches the browser into its built-in dark mode, which includes a black default background, white default text, \u201cdark\u201d styles for scrollbars, and other elements that are difficult to target with CSS, and more. I\u2019m using CSS variables to hint that the value is dynamic — and because I like the browser developer tools experience — but plain color-scheme: light<\/code> and color-scheme: dark<\/code> would work fine.<\/p>\n:root {\n \/* light styles here *\/\n color-scheme: var(--color-scheme, light);\n \n \/* system preference is \"dark\" *\/\n @media (prefers-color-scheme: dark) {\n --color-scheme: dark;\n \/* any additional dark styles here *\/\n }\n}\n<\/code><\/pre>\nGiving Users Control<\/h2>\n
Now, to support overriding<\/em> the system preference, let users choose between light (default) and dark color schemes at the page level.<\/p>\nHTML has native elements for handling user interactions. Using one of those controls, rather than, say, a <div><\/code> nest, improves the chances that assistive tech users will have a good experience. I\u2019ll use a <select><\/code> menu with options for \u201csystem,\u201d \u201clight,\u201d and \u201cdark.\u201d A group of <input type="radio"><\/code> would work, too, if you wanted the options right on the surface instead of a dropdown menu.<\/p>\n<select id=\"color-scheme\">\n <option value=\"system\" selected>System<\/option>\n <option value=\"light\">Light<\/option>\n <option value=\"dark\">Dark<\/option>\n<\/select>\n<\/code><\/pre>\nBefore CSS gained :has()<\/code>, responding to the user\u2019s selected <option><\/code> required JavaScript, for example, setting an event listener on the <select><\/code> to toggle a class or attribute on <html><\/code> or <body><\/code>.<\/p>\nBut now that we have :has()<\/code>, we can now do this with CSS alone! You\u2019ll save spending any of your performance budget on a dark mode script, plus the control will work even for users who have disabled JavaScript. And any \u201cno-JS\u201d folks on the project will be satisfied.<\/p>\nWhat we need is a selector that applies to the page when it :has()<\/code> a select<\/code> menu with a particular [value]:checked<\/code>. Let\u2019s translate that into CSS:<\/p>\n:root:has(select option[value=\"dark\"]:checked)<\/code><\/pre>\nWe\u2019re defaulting to a light color scheme, so it\u2019s enough to account for two possible dark color scheme scenarios:<\/p>\n
\n- The page-level color preference is \u201csystem,\u201d and the system-level preference is \u201cdark.\u201d<\/li>\n
- The page-level color preference is \u201cdark\u201d.<\/li>\n<\/ol>\n
The first one is a page-preference-aware iteration of our prefers-color-scheme: dark<\/code> case. A \u201cdark\u201d system-level preference is no longer enough to warrant dark styles; we need a \u201cdark\u201d system-level preference and a \u201cfollow the system-level preference\u201d at the page-level preference. We\u2019ll wrap the prefers-color-scheme<\/code> media query dark scheme styles with the :has()<\/code> selector we just wrote:<\/p>\n\n:root {\n \/* light styles here *\/\n color-scheme: var(--color-scheme, light);\n \n \/* page preference is \"system\", and system preference is \"dark\" *\/\n @media (prefers-color-scheme: dark) {\n &:has(#color-scheme option[value=\"system\"]:checked) {\n --color-scheme: dark;\n \/* any additional dark styles, again *\/\n }\n }\n}\n<\/code><\/pre>\n<\/div>\nNotice that I\u2019m using CSS Nesting<\/a> in that last snippet. Baseline 2023<\/a> has it pegged as \u201cNewly available across major browsers\u201d which means support is good, but at the time of writing, support on Android browsers not included in Baseline\u2019s core browser set<\/a> is limited<\/a>. You can get the same result without nesting.<\/p>\n\n:root {\n \/* light styles *\/\n color-scheme: var(--color-scheme, light);\n \n \/* page preference is \"dark\" *\/\n &:has(#color-scheme option[value=\"dark\"]:checked) {\n --color-scheme: dark;\n \/* any additional dark styles *\/\n }\n}\n<\/code><\/pre>\n<\/div>\nFor the second dark mode scenario, we\u2019ll use nearly the exact same :has()<\/code> selector as we did for the first scenario, this time checking whether the \u201cdark\u201d option — rather than the \u201csystem\u201d option — is selected:<\/p>\n\n:root {\n \/* light styles *\/\n color-scheme: var(--color-scheme, light);\n \n \/* page preference is \"dark\" *\/\n &:has(#color-scheme option[value=\"dark\"]:checked) {\n --color-scheme: dark;\n \/* any additional dark styles *\/\n }\n \n \/* page preference is \"system\", and system preference is \"dark\" *\/\n @media (prefers-color-scheme: dark) {\n &:has(#color-scheme option[value=\"system\"]:checked) {\n --color-scheme: dark;\n \/* any additional dark styles, again *\/\n }\n }\n}\n<\/code><\/pre>\n<\/div>\nNow the page\u2019s styles respond to both changes in users\u2019 system settings and<\/em> user interaction with the page\u2019s color preference UI — all with CSS!<\/p>\nBut the colors change instantly<\/em>. Let\u2019s smooth the transition.<\/p>\nRespecting Motion Preferences<\/h2>\n
Instantaneous style changes can feel inelegant in some cases, and this is one of them. So, let\u2019s apply a CSS transition on the :root<\/code> to \u201cease\u201d the switch between color schemes. (Transition styles at the :root<\/code> will cascade down to the rest of the page, which may necessitate adding transition: none<\/code> or other transition overrides.)<\/p>\nNote that the CSS color-scheme<\/code> property does not support transitions.<\/p>\n\n:root {\n transition-duration: 200ms;\n transition-property: \/* properties changed by your light\/dark styles *\/;\n}\n<\/code><\/pre>\n<\/div>\nNot all users will consider the addition of a transition a welcome improvement. Querying the prefers-reduced-motion<\/code><\/a> media feature allows us to account for a user\u2019s motion preferences. If the value is set to reduce<\/code>, then we remove the transition-duration<\/code> to eliminate unwanted motion.<\/p>\n\n:root {\n transition-duration: 200ms;\n transition-property: \/* properties changed by your light\/dark styles *\/;\n \n @media screen and (prefers-reduced-motion: reduce) {\n transition-duration: none;\n }\n}\n<\/code><\/pre>\n<\/div>\nTransitions can also produce poor user experiences on devices that render changes slowly, for example, ones with e-ink screens. We can extend our \u201cno motion condition\u201d media query to account for that with the update<\/code><\/a> media feature. If its value is slow<\/code>, then we remove the transition-duration<\/code>.<\/p>\n\n:root {\n transition-duration: 200ms;\n transition-property: \/* properties changed by your light\/dark styles *\/;\n \n @media screen and (prefers-reduced-motion: reduce), (update: slow) {\n transition-duration: 0s;\n }\n}\n<\/code><\/pre>\n<\/div>\nLet\u2019s try out what we have so far in the following demo. Notice that, to work around color-scheme<\/code>\u2019s lack of transition support, I\u2019ve explicitly styled the properties that should transition during theme changes.<\/p>\n\nSee the Pen [CSS-only theme switcher (requires :has()) [forked]](https:\/\/codepen.io\/smashingmag\/pen\/YzMVQja) by Henry<\/a>.<\/p>See the Pen CSS-only theme switcher (requires :has()) [forked]<\/a> by Henry<\/a>.<\/figcaption><\/figure>\nNot bad! But what happens if the user refreshes the pages or navigates to another page? The reload effectively wipes out the user\u2019s form selection, forcing the user to re-make the selection. That may be acceptable in some contexts, but it\u2019s likely to go against user expectations. Let\u2019s bring in JavaScript for a touch of progressive enhancement in the form of\u2026<\/p>\n
Persistence<\/h2>\n
Here\u2019s a vanilla JavaScript implementation. It\u2019s a naive starting point — the functions and variables aren\u2019t encapsulated but are instead properties on window<\/code>. You\u2019ll want to adapt this in a way that fits your site\u2019s conventions, framework, library, and so on.<\/p>\nWhen the user changes the color scheme from the <select><\/code> menu, we\u2019ll store the selected <option><\/code> value in a new localStorage<\/code> item called "preferredColorScheme"<\/code>. On subsequent page loads, we\u2019ll check localStorage<\/code> for the "preferredColorScheme"<\/code> item. If it exists, and if its value corresponds to one of the form control options, we restore the user\u2019s preference by programmatically updating the menu selection.<\/p>\n\n\/*\n * If a color scheme preference was previously stored,\n * select the corresponding option in the color scheme preference UI\n * unless it is already selected.\n *\/\nfunction restoreColorSchemePreference() {\n const colorScheme = localStorage.getItem(colorSchemeStorageItemName);\n\n if (!colorScheme) {\n \/\/ There is no stored preference to restore\n return;\n }\n\n const option = colorSchemeSelectorEl.querySelector(`[value=${colorScheme}]`); \n\n if (!option) {\n \/\/ The stored preference has no corresponding option in the UI.\n localStorage.removeItem(colorSchemeStorageItemName);\n return;\n }\n\n if (option.selected) { \n \/\/ The stored preference's corresponding menu option is already selected\n return;\n }\n\n option.selected = true;\n}\n\n\/*\n * Store an event target's value in localStorage under colorSchemeStorageItemName\n *\/\nfunction storeColorSchemePreference({ target }) {\n const colorScheme = target.querySelector(\":checked\").value;\n localStorage.setItem(colorSchemeStorageItemName, colorScheme);\n}\n\n\/\/ The name under which the user's color scheme preference will be stored.\nconst colorSchemeStorageItemName = \"preferredColorScheme\";\n\n\/\/ The color scheme preference front-end UI.\nconst colorSchemeSelectorEl = document.querySelector(\"#color-scheme\");\n\nif (colorSchemeSelectorEl) {\n restoreColorSchemePreference();\n\n \/\/ When the user changes their color scheme preference via the UI,\n \/\/ store the new preference.\n colorSchemeSelectorEl.addEventListener(\"input\", storeColorSchemePreference);\n}\n<\/code><\/pre>\n<\/div>\nLet\u2019s try that out. Open this demo (perhaps in a new window), use the menu to change the color scheme, and then refresh the page to see your preference persist:<\/p>\n\nSee the Pen [CSS-only theme switcher (requires :has()) with JS persistence [forked]](https:\/\/codepen.io\/smashingmag\/pen\/GRLmEXX) by Henry<\/a>.<\/p>See the Pen CSS-only theme switcher (requires :has()) with JS persistence [forked]<\/a> by Henry<\/a>.<\/figcaption><\/figure>\nIf your system color scheme preference is \u201clight\u201d and you set the demo\u2019s color scheme to \u201cdark,\u201d you may get the light mode styles for a moment immediately after reloading the page before the dark mode styles kick in. That\u2019s because CodePen loads its own JavaScript before the demo\u2019s scripts. That is out of my control, but you can take care to improve this persistence on your projects.<\/p>\n
Persistence Performance Considerations<\/h2>\n
Where things can get tricky is restoring the user\u2019s preference immediately<\/em> after the page loads. If the color scheme preference in localStorage<\/code> is different from the user\u2019s system-level color scheme preference, it\u2019s possible the user will see the system preference color scheme before the page-level preference is restored. (Users who have selected the \u201cSystem\u201d option will never get that flash; neither will those whose system settings match their selected option in the form control.)<\/p>\nIf your implementation is showing a \u201cflash of inaccurate color theme\u201d<\/a>, where is the problem happening? Generally speaking, the earlier the scripts appear on the page, the lower the risk. The \u201cbest option\u201d for you will depend on your specific stack, of course.<\/p>\nWhat About Browsers That Don\u2019t Support :has()<\/code>?<\/h2>\n
\n 2024-10-15T23:05:45+00:00
\n <\/header>\n
<body><\/code> element in response, and write the styles for that class to override design with a different color scheme.<\/p>\nCSS\u2019s new :has()<\/code> pseudo-class<\/a>, supported by major browsers since December 2023, opens many doors for front-end developers. I\u2019m especially excited about leveraging it to modify UI in response to user interaction without JavaScript<\/em>. Where previously we have used JavaScript to toggle classes or attributes (or to set styles directly), we can now pair :has()<\/code> selectors with HTML\u2019s native interactive elements.<\/p>\nSupporting a color scheme preference, like \u201cDark Mode,\u201d is a great use case. We can use a <select><\/code> element anywhere that toggles color schemes based on the selected <option><\/code> — no JavaScript needed, save for a sprinkle to save the user\u2019s choice, which we\u2019ll get to further in.<\/p>\nRespecting System Preferences<\/h2>\n
First, we\u2019ll support a user\u2019s system-wide color scheme preferences by adopting a \u201cLight Mode\u201d-first approach. In other words, we start with a light color scheme by default and swap it out for a dark color scheme for users who prefer it.<\/p>\n
The prefers-color-scheme<\/code><\/a> media feature detects the user\u2019s system preference. Wrap \u201cdark\u201d styles in a prefers-color-scheme: dark<\/code> media query.<\/p>\nselector {\n \/* light styles *\/\n\n @media (prefers-color-scheme: dark) {\n \/* dark styles *\/\n }\n}\n<\/code><\/pre>\nNext, set the color-scheme<\/code><\/a> property to match the preferred color scheme. Setting color-scheme: dark<\/code> switches the browser into its built-in dark mode, which includes a black default background, white default text, \u201cdark\u201d styles for scrollbars, and other elements that are difficult to target with CSS, and more. I\u2019m using CSS variables to hint that the value is dynamic — and because I like the browser developer tools experience — but plain color-scheme: light<\/code> and color-scheme: dark<\/code> would work fine.<\/p>\n:root {\n \/* light styles here *\/\n color-scheme: var(--color-scheme, light);\n \n \/* system preference is \"dark\" *\/\n @media (prefers-color-scheme: dark) {\n --color-scheme: dark;\n \/* any additional dark styles here *\/\n }\n}\n<\/code><\/pre>\nGiving Users Control<\/h2>\n
Now, to support overriding<\/em> the system preference, let users choose between light (default) and dark color schemes at the page level.<\/p>\nHTML has native elements for handling user interactions. Using one of those controls, rather than, say, a <div><\/code> nest, improves the chances that assistive tech users will have a good experience. I\u2019ll use a <select><\/code> menu with options for \u201csystem,\u201d \u201clight,\u201d and \u201cdark.\u201d A group of <input type="radio"><\/code> would work, too, if you wanted the options right on the surface instead of a dropdown menu.<\/p>\n<select id=\"color-scheme\">\n <option value=\"system\" selected>System<\/option>\n <option value=\"light\">Light<\/option>\n <option value=\"dark\">Dark<\/option>\n<\/select>\n<\/code><\/pre>\nBefore CSS gained :has()<\/code>, responding to the user\u2019s selected <option><\/code> required JavaScript, for example, setting an event listener on the <select><\/code> to toggle a class or attribute on <html><\/code> or <body><\/code>.<\/p>\nBut now that we have :has()<\/code>, we can now do this with CSS alone! You\u2019ll save spending any of your performance budget on a dark mode script, plus the control will work even for users who have disabled JavaScript. And any \u201cno-JS\u201d folks on the project will be satisfied.<\/p>\nWhat we need is a selector that applies to the page when it :has()<\/code> a select<\/code> menu with a particular [value]:checked<\/code>. Let\u2019s translate that into CSS:<\/p>\n:root:has(select option[value=\"dark\"]:checked)<\/code><\/pre>\nWe\u2019re defaulting to a light color scheme, so it\u2019s enough to account for two possible dark color scheme scenarios:<\/p>\n
\n- The page-level color preference is \u201csystem,\u201d and the system-level preference is \u201cdark.\u201d<\/li>\n
- The page-level color preference is \u201cdark\u201d.<\/li>\n<\/ol>\n
The first one is a page-preference-aware iteration of our prefers-color-scheme: dark<\/code> case. A \u201cdark\u201d system-level preference is no longer enough to warrant dark styles; we need a \u201cdark\u201d system-level preference and a \u201cfollow the system-level preference\u201d at the page-level preference. We\u2019ll wrap the prefers-color-scheme<\/code> media query dark scheme styles with the :has()<\/code> selector we just wrote:<\/p>\n\n:root {\n \/* light styles here *\/\n color-scheme: var(--color-scheme, light);\n \n \/* page preference is \"system\", and system preference is \"dark\" *\/\n @media (prefers-color-scheme: dark) {\n &:has(#color-scheme option[value=\"system\"]:checked) {\n --color-scheme: dark;\n \/* any additional dark styles, again *\/\n }\n }\n}\n<\/code><\/pre>\n<\/div>\nNotice that I\u2019m using CSS Nesting<\/a> in that last snippet. Baseline 2023<\/a> has it pegged as \u201cNewly available across major browsers\u201d which means support is good, but at the time of writing, support on Android browsers not included in Baseline\u2019s core browser set<\/a> is limited<\/a>. You can get the same result without nesting.<\/p>\n\n:root {\n \/* light styles *\/\n color-scheme: var(--color-scheme, light);\n \n \/* page preference is \"dark\" *\/\n &:has(#color-scheme option[value=\"dark\"]:checked) {\n --color-scheme: dark;\n \/* any additional dark styles *\/\n }\n}\n<\/code><\/pre>\n<\/div>\nFor the second dark mode scenario, we\u2019ll use nearly the exact same :has()<\/code> selector as we did for the first scenario, this time checking whether the \u201cdark\u201d option — rather than the \u201csystem\u201d option — is selected:<\/p>\n\n:root {\n \/* light styles *\/\n color-scheme: var(--color-scheme, light);\n \n \/* page preference is \"dark\" *\/\n &:has(#color-scheme option[value=\"dark\"]:checked) {\n --color-scheme: dark;\n \/* any additional dark styles *\/\n }\n \n \/* page preference is \"system\", and system preference is \"dark\" *\/\n @media (prefers-color-scheme: dark) {\n &:has(#color-scheme option[value=\"system\"]:checked) {\n --color-scheme: dark;\n \/* any additional dark styles, again *\/\n }\n }\n}\n<\/code><\/pre>\n<\/div>\nNow the page\u2019s styles respond to both changes in users\u2019 system settings and<\/em> user interaction with the page\u2019s color preference UI — all with CSS!<\/p>\nBut the colors change instantly<\/em>. Let\u2019s smooth the transition.<\/p>\nRespecting Motion Preferences<\/h2>\n
Instantaneous style changes can feel inelegant in some cases, and this is one of them. So, let\u2019s apply a CSS transition on the :root<\/code> to \u201cease\u201d the switch between color schemes. (Transition styles at the :root<\/code> will cascade down to the rest of the page, which may necessitate adding transition: none<\/code> or other transition overrides.)<\/p>\nNote that the CSS color-scheme<\/code> property does not support transitions.<\/p>\n\n:root {\n transition-duration: 200ms;\n transition-property: \/* properties changed by your light\/dark styles *\/;\n}\n<\/code><\/pre>\n<\/div>\nNot all users will consider the addition of a transition a welcome improvement. Querying the prefers-reduced-motion<\/code><\/a> media feature allows us to account for a user\u2019s motion preferences. If the value is set to reduce<\/code>, then we remove the transition-duration<\/code> to eliminate unwanted motion.<\/p>\n\n:root {\n transition-duration: 200ms;\n transition-property: \/* properties changed by your light\/dark styles *\/;\n \n @media screen and (prefers-reduced-motion: reduce) {\n transition-duration: none;\n }\n}\n<\/code><\/pre>\n<\/div>\nTransitions can also produce poor user experiences on devices that render changes slowly, for example, ones with e-ink screens. We can extend our \u201cno motion condition\u201d media query to account for that with the update<\/code><\/a> media feature. If its value is slow<\/code>, then we remove the transition-duration<\/code>.<\/p>\n\n:root {\n transition-duration: 200ms;\n transition-property: \/* properties changed by your light\/dark styles *\/;\n \n @media screen and (prefers-reduced-motion: reduce), (update: slow) {\n transition-duration: 0s;\n }\n}\n<\/code><\/pre>\n<\/div>\nLet\u2019s try out what we have so far in the following demo. Notice that, to work around color-scheme<\/code>\u2019s lack of transition support, I\u2019ve explicitly styled the properties that should transition during theme changes.<\/p>\n\nSee the Pen [CSS-only theme switcher (requires :has()) [forked]](https:\/\/codepen.io\/smashingmag\/pen\/YzMVQja) by Henry<\/a>.<\/p>See the Pen CSS-only theme switcher (requires :has()) [forked]<\/a> by Henry<\/a>.<\/figcaption><\/figure>\nNot bad! But what happens if the user refreshes the pages or navigates to another page? The reload effectively wipes out the user\u2019s form selection, forcing the user to re-make the selection. That may be acceptable in some contexts, but it\u2019s likely to go against user expectations. Let\u2019s bring in JavaScript for a touch of progressive enhancement in the form of\u2026<\/p>\n
Persistence<\/h2>\n
Here\u2019s a vanilla JavaScript implementation. It\u2019s a naive starting point — the functions and variables aren\u2019t encapsulated but are instead properties on window<\/code>. You\u2019ll want to adapt this in a way that fits your site\u2019s conventions, framework, library, and so on.<\/p>\nWhen the user changes the color scheme from the <select><\/code> menu, we\u2019ll store the selected <option><\/code> value in a new localStorage<\/code> item called "preferredColorScheme"<\/code>. On subsequent page loads, we\u2019ll check localStorage<\/code> for the "preferredColorScheme"<\/code> item. If it exists, and if its value corresponds to one of the form control options, we restore the user\u2019s preference by programmatically updating the menu selection.<\/p>\n\n\/*\n * If a color scheme preference was previously stored,\n * select the corresponding option in the color scheme preference UI\n * unless it is already selected.\n *\/\nfunction restoreColorSchemePreference() {\n const colorScheme = localStorage.getItem(colorSchemeStorageItemName);\n\n if (!colorScheme) {\n \/\/ There is no stored preference to restore\n return;\n }\n\n const option = colorSchemeSelectorEl.querySelector(`[value=${colorScheme}]`); \n\n if (!option) {\n \/\/ The stored preference has no corresponding option in the UI.\n localStorage.removeItem(colorSchemeStorageItemName);\n return;\n }\n\n if (option.selected) { \n \/\/ The stored preference's corresponding menu option is already selected\n return;\n }\n\n option.selected = true;\n}\n\n\/*\n * Store an event target's value in localStorage under colorSchemeStorageItemName\n *\/\nfunction storeColorSchemePreference({ target }) {\n const colorScheme = target.querySelector(\":checked\").value;\n localStorage.setItem(colorSchemeStorageItemName, colorScheme);\n}\n\n\/\/ The name under which the user's color scheme preference will be stored.\nconst colorSchemeStorageItemName = \"preferredColorScheme\";\n\n\/\/ The color scheme preference front-end UI.\nconst colorSchemeSelectorEl = document.querySelector(\"#color-scheme\");\n\nif (colorSchemeSelectorEl) {\n restoreColorSchemePreference();\n\n \/\/ When the user changes their color scheme preference via the UI,\n \/\/ store the new preference.\n colorSchemeSelectorEl.addEventListener(\"input\", storeColorSchemePreference);\n}\n<\/code><\/pre>\n<\/div>\nLet\u2019s try that out. Open this demo (perhaps in a new window), use the menu to change the color scheme, and then refresh the page to see your preference persist:<\/p>\n\nSee the Pen [CSS-only theme switcher (requires :has()) with JS persistence [forked]](https:\/\/codepen.io\/smashingmag\/pen\/GRLmEXX) by Henry<\/a>.<\/p>See the Pen CSS-only theme switcher (requires :has()) with JS persistence [forked]<\/a> by Henry<\/a>.<\/figcaption><\/figure>\nIf your system color scheme preference is \u201clight\u201d and you set the demo\u2019s color scheme to \u201cdark,\u201d you may get the light mode styles for a moment immediately after reloading the page before the dark mode styles kick in. That\u2019s because CodePen loads its own JavaScript before the demo\u2019s scripts. That is out of my control, but you can take care to improve this persistence on your projects.<\/p>\n
Persistence Performance Considerations<\/h2>\n
Where things can get tricky is restoring the user\u2019s preference immediately<\/em> after the page loads. If the color scheme preference in localStorage<\/code> is different from the user\u2019s system-level color scheme preference, it\u2019s possible the user will see the system preference color scheme before the page-level preference is restored. (Users who have selected the \u201cSystem\u201d option will never get that flash; neither will those whose system settings match their selected option in the form control.)<\/p>\nIf your implementation is showing a \u201cflash of inaccurate color theme\u201d<\/a>, where is the problem happening? Generally speaking, the earlier the scripts appear on the page, the lower the risk. The \u201cbest option\u201d for you will depend on your specific stack, of course.<\/p>\nWhat About Browsers That Don\u2019t Support :has()<\/code>?<\/h2>\n