<main class="card">
<header class="card__header header">
<label class="header__selection toggle">
<input js-toggle class="toggle__input" aria-label="toggle dark mode" type="checkbox">
<span class="toggle__fake"></span>
<h1 class="toggle__text">Dark Mode</h1>
</label>
</header>
<section class="card__body body">
<p class="body__text">The <code>prefers-color-scheme</code> CSS 5 Media Query is used to detect if the user has requested the system to use a light or dark color theme. This Pen demonstrates how to use a theme toggle for both legacy and progressive code. Check <a target="_blank" rel="noopener" href="//caniuse.com/#feat=prefers-color-scheme" aria-label="Can I Use Website">Caniuse</a> for support.</p>
<p class="body__text">If the theme is overwriten by the toggle, it will be persisted via local storage; else the media query will keep that setting. Press the following button to clear that storage.</p>
</section>
<section class="card__body card__body--right body">
<button type="button" js-clearStorage>Clear Local Storage</button>
</section>
</main>
@mixin includeLight {
$base-text-color: 0,0,0;
--background-color: #eeeeee;
--background-card-color: #ffffff;
--background-success-color: #85da85;
--background-toggle-inactive-color: rgba(#{$base-text-color}, 0);
--background-toggle-active-color: rgba(#{$base-text-color}, 0.08);
--foreground-color: rgba(#{$base-text-color}, 0.87);
--foreground-success-color: #000000;
--foreground-secondary-color: rgba(#{$base-text-color}, 0.56);
--focus-color: rgba(#{$base-text-color}, 0.5);
}
@mixin includeDark {
$base-text-color: 255,255,255;
--background-color: #212121;
--background-card-color: #363636;
--background-success-color: #008000;
--background-toggle-inactive-color: rgba(#{$base-text-color}, 0);
--background-toggle-active-color: rgba(#{$base-text-color}, 0.1);
--foreground-color: rgba(#{$base-text-color}, 0.87);
--foreground-success-color: #ffffff;
--foreground-secondary-color: rgba(#{$base-text-color}, 0.56);
--focus-color: rgba(#{$base-text-color}, 0.5);
}
%focus {
outline-color: var(--focus-color);
outline-style: solid;
outline-width: 2px;
outline-offset: 2px;
}
html, html.light { @include includeLight }
html.dark { @include includeDark }
@media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) {
html { @include includeLight }
html.dark { @include includeDark }
}
@media (prefers-color-scheme: dark) {
html { @include includeDark }
html.light { @include includeLight }
}
* {
box-sizing: border-box;
&:focus {
@extend %focus;
}
}
html, body {
width: 100%;
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
font-family: 'Roboto', sans-serif;
background-color: var(--background-color);
}
h1 {
font-size: 20px;
color: var(--foreground-color);
}
p {
color: var(--foreground-secondary-color);
line-height: 1.5;
margin-bottom: 16px;
}
a {
color: var(--foreground-color);
text-transform: uppercase;
}
code {
display: inline-block;
padding: 2px 6px;
background-color: var(--background-toggle-active-color);
border-radius: 2px;
}
input[type="checkbox"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
opacity: 0;
cursor: inherit;
}
.card {
width: 100%;
max-width: 600px;
margin: 8px 16px;
padding: 16px 24px 8px;
background-color: var(--background-card-color);
border-radius: 8px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
&__header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
&__body {
padding-top: 16px;
&--right {
display: flex;
justify-content: flex-end;
padding-top: 0;
}
}
}
.header {
&__text {
padding: 8px 0;
margin-right: 16px;
}
}
.footer {
&__p {
padding: 8px 24px;
margin: 0 0 16px -24px;
border-radius: 0 22px 22px 0;
background-color: var(--background-success-color);
color: var(--foreground-success-color);
}
}
.toggle {
flex: 1;
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
cursor: pointer;
&__input {
&:checked {
~ .toggle__fake {
&::after {
left: 30px;
background-color: var(--foreground-color);
}
}
}
&:focus,
&:not([disabled]):hover {
~ .toggle__fake {
background-color: var(--background-toggle-active-color);
}
}
&:focus-visible {
~ .toggle__fake {
@extend %focus;
}
}
}
&__fake {
will-change: background-color;
position: relative;
display: inline-block;
width: 52px;
height: 24px;
background-color: var(--background-toggle-inactive-color);
border: 1px solid var(--foreground-color);
border-radius: 12px;
transition: background-color 100ms cubic-bezier(0.4, 0.0, 0.2, 1);
&::after {
will-change: left;
content: '';
position: absolute;
top: 50%;
left: 4px;
display: block;
width: 15px;
height: 15px;
background-color: var(--background-card-color);
border: 1px solid var(--foreground-color);
border-radius: 8px;
transform: translateY(-50%);
transition: left 100ms cubic-bezier(0.4, 0.0, 0.2, 1);
}
}
&__text {
margin-left: 16px;
color: var(--foreground-color);
}
}
button {
appearance: none;
display: inline-flex;
align-items: center;
min-height: 32px;
margin: 8px 0 16px;
padding: 8px;
color: var(--foreground-color);
text-transform: uppercase;
letter-spacing: 0.1em;
background-color: var(--background-color);
border: 1px solid var(--foreground-color);
border-radius: 4px;
cursor: pointer;
}
View Compiled
const themePreference = () => {
const hasLocalStorage = localStorage.getItem('theme');
let supports = false;
let theme = undefined;
if (hasLocalStorage === 'light') { theme = 'light'; }
if (hasLocalStorage === 'dark') { theme = 'dark'; }
if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) {
theme = hasLocalStorage ? hasLocalStorage : 'dark';
supports = true;
};
if (window.matchMedia(`(prefers-color-scheme: light)`).matches) {
theme = hasLocalStorage ? hasLocalStorage : 'light';
supports = true;
};
if (window.matchMedia(`(prefers-color-scheme: no-preference)`).matches) {
theme = hasLocalStorage ? hasLocalStorage : 'none';
supports = true;
};
return {supports, theme};
}
addFooter = () => {
const parent = document.querySelector('.card');
const footer = document.createElement('footer');
const html = `
<p class="footer__p"><span aria-label="checkmark:">✔</span> Your system supports the CSS 5 Media Query <code>prefers-color-scheme</code>.</p>
`;
footer.classList.add('card__footer', 'footer');
footer.innerHTML = html;
parent.appendChild(footer);
}
document.addEventListener('DOMContentLoaded', e => {
console.clear();
const userThemePreference = themePreference();
const toggle = document.querySelector('[js-toggle]');
const clearStorage = document.querySelector('[js-clearStorage]');
const html = document.documentElement;
const setTheme = () => {
switch(userThemePreference.theme) {
case 'dark':
toggle.checked = true;
html.classList.add('dark');
html.classList.remove('light');
break;
case 'light':
toggle.checked = false;
html.classList.remove('dark');
html.classList.add('light');
break;
}
}
setTheme();
if (userThemePreference.supports) {
addFooter();
}
clearStorage.addEventListener('click', e => {
localStorage.removeItem('theme');
console.info('local storage cleared');
}, false);
toggle.addEventListener('click', e => {
if (toggle.checked) {
html.classList.add('dark');
html.classList.remove('light');
localStorage.setItem('theme', 'dark');
} else {
html.classList.remove('dark');
html.classList.add('light');
localStorage.setItem('theme', 'light');
}
}, false);
}, false);
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.