Redesign theme toggle as a two-icon light/dark switch
Replace the single-icon popover with a dropdown of radio inputs with a compact two-button toggle (light/dark), inspired by vale.rocks' design: clicking an icon forces that theme, clicking the active icon again returns to following the system preference. Keeps the existing light-dark() colour-scheme mechanism and view-transition wipe animation, and reuses the project's existing Fluent UI sun/moon icons. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn4aNjE3ofpDM78F4qmdCV
This commit is contained in:
parent
be2bc267ad
commit
e9d8f4c74b
7 changed files with 102 additions and 136 deletions
|
|
@ -1,72 +1,68 @@
|
|||
@layer components {
|
||||
#theme-selector {
|
||||
display: flex;
|
||||
justify-self: start;
|
||||
align-items: center;
|
||||
gap: .25rem;
|
||||
padding: .25rem;
|
||||
border-radius: 999px;
|
||||
background-color: color-mix(in oklch, var(--clr-border) 40%, transparent);
|
||||
|
||||
button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 999px;
|
||||
padding: .375rem;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
transition: background-color .2s, color .2s;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* This is the element with the [popover] attribute */
|
||||
#theme-selector-dropdown {
|
||||
position: absolute;
|
||||
position-area: block-end span-inline-start;
|
||||
margin: 0;
|
||||
border: 2px solid var(--clr-border);
|
||||
background-color: var(--clr-background);
|
||||
color: var(--clr-text);
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1ch;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--clr-text);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the element with the [popover] attribute
|
||||
* Here we are styling the open and closing animations
|
||||
*/
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
#theme-selector-dropdown {
|
||||
&:popover-open {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
svg {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
/*
|
||||
* Start styles for the opening transition.
|
||||
* Added to :popover-open, but after the opened styles.
|
||||
*/
|
||||
@starting-style {
|
||||
transform: translateY(30px) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
background-color: color-mix(in oklch, var(--clr-border) 70%, transparent);
|
||||
}
|
||||
|
||||
/*
|
||||
* End styles for the closing transition.
|
||||
*/
|
||||
transform: translateY(0) scale(0);
|
||||
opacity: 0;
|
||||
&[aria-pressed="true"]:hover {
|
||||
background-color: var(--clr-text);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Enumerate transitioning properties, including display and overlay.
|
||||
*/
|
||||
transition: transform, opacity, display allow-discrete, overlay allow-discrete;
|
||||
transition-duration: 0.5s;
|
||||
transform-origin: top right;
|
||||
&[aria-pressed="true"] {
|
||||
background-color: var(--clr-text);
|
||||
color: var(--clr-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
::view-transition-group(changing-theme) {
|
||||
animation-duration: 1.25s;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +1,60 @@
|
|||
class ThemeSelector {
|
||||
constructor() {
|
||||
this.widget = document.querySelector('#theme-selector');
|
||||
this.btnLight = this.widget.querySelector('#btn-light');
|
||||
this.btnDark = this.widget.querySelector('#btn-dark');
|
||||
this.status = this.widget.querySelector('#theme-status');
|
||||
this.currentTheme = 'system';
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.widget.querySelectorAll('#theme-selector-dropdown input').forEach((radioInput) => radioInput.addEventListener('input', (e) => {
|
||||
let theme = e.target.value;
|
||||
this.btnLight.addEventListener('click', () => {
|
||||
this.applyTheme(this.currentTheme === 'light' ? 'system' : 'light');
|
||||
});
|
||||
|
||||
// Update current icon
|
||||
this.widget.querySelectorAll('.toggle svg').forEach((svg) => {
|
||||
if (svg.classList.contains(theme)) {
|
||||
svg.style.display = '';
|
||||
} else {
|
||||
svg.style.display = 'none';
|
||||
}
|
||||
});
|
||||
this.btnDark.addEventListener('click', () => {
|
||||
this.applyTheme(this.currentTheme === 'dark' ? 'system' : 'dark');
|
||||
});
|
||||
}
|
||||
|
||||
// Set the theme
|
||||
let selectedTheme;
|
||||
switch (theme) {
|
||||
case 'dark':
|
||||
case 'light':
|
||||
selectedTheme = theme;
|
||||
break;
|
||||
default:
|
||||
selectedTheme = 'light dark';
|
||||
}
|
||||
applyTheme(theme) {
|
||||
const selectedTheme = (theme === 'light' || theme === 'dark') ? theme : 'light dark';
|
||||
|
||||
const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
||||
const html = document.querySelector('html');
|
||||
let currentTheme = html.style.getPropertyValue('color-scheme');
|
||||
if (currentTheme === '') {
|
||||
currentTheme = 'light dark';
|
||||
}
|
||||
const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
||||
const html = document.documentElement;
|
||||
let currentColorScheme = html.style.getPropertyValue('color-scheme');
|
||||
if (currentColorScheme === '') {
|
||||
currentColorScheme = 'light dark';
|
||||
}
|
||||
|
||||
/* do we need to transition */
|
||||
let doTransition = false;
|
||||
/* do we need to transition */
|
||||
let doTransition = false;
|
||||
|
||||
if (selectedTheme !== currentTheme) {
|
||||
if (selectedTheme === 'light dark') {
|
||||
doTransition = currentTheme !== systemTheme;
|
||||
} else if (currentTheme === 'light dark') {
|
||||
doTransition = selectedTheme !== systemTheme;
|
||||
} else {
|
||||
doTransition = true;
|
||||
}
|
||||
}
|
||||
|
||||
html.style.viewTransitionName = 'changing-theme';
|
||||
if (doTransition && document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
// Close the popover
|
||||
document.querySelector('#theme-selector-dropdown').togglePopover();
|
||||
|
||||
// Set the colour theme
|
||||
html.style.setProperty('color-scheme', selectedTheme);
|
||||
});
|
||||
if (selectedTheme !== currentColorScheme) {
|
||||
if (selectedTheme === 'light dark') {
|
||||
doTransition = currentColorScheme !== systemTheme;
|
||||
} else if (currentColorScheme === 'light dark') {
|
||||
doTransition = selectedTheme !== systemTheme;
|
||||
} else {
|
||||
// Close the popover
|
||||
document.querySelector('#theme-selector-dropdown').togglePopover();
|
||||
|
||||
// Set the colour theme
|
||||
html.style.setProperty('color-scheme', selectedTheme);
|
||||
doTransition = true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const applyChange = () => {
|
||||
this.currentTheme = theme;
|
||||
html.style.setProperty('color-scheme', selectedTheme);
|
||||
|
||||
this.btnLight.setAttribute('aria-pressed', String(theme === 'light'));
|
||||
this.btnDark.setAttribute('aria-pressed', String(theme === 'dark'));
|
||||
this.status.textContent = theme === 'system' ? 'Theme set to system default' : '';
|
||||
};
|
||||
|
||||
html.style.viewTransitionName = 'changing-theme';
|
||||
if (doTransition && document.startViewTransition) {
|
||||
document.startViewTransition(applyChange);
|
||||
} else {
|
||||
applyChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,35 +38,14 @@
|
|||
<a href="/projects">Projects</a>
|
||||
<a href="{{ route('feed.notes.json') }}" class="rss-icon">@include('icons.json-feed', ['title' => 'JSON Feed'])</a>
|
||||
</nav>
|
||||
<div id="theme-selector">
|
||||
<button class="toggle" popovertarget="theme-selector-dropdown">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="system" style=""><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10m0-1.5v-17a8.5 8.5 0 0 1 0 17"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="dark" style="display: none"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M20.026 17.001c-2.762 4.784-8.879 6.423-13.663 3.661A10 10 0 0 1 3.13 17.68a.75.75 0 0 1 .365-1.132c3.767-1.348 5.785-2.91 6.956-5.146c1.233-2.353 1.551-4.93.689-8.463a.75.75 0 0 1 .769-.927a9.96 9.96 0 0 1 4.457 1.327c4.784 2.762 6.423 8.879 3.66 13.662"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="light" style="display: none"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M12 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 2m5 10a5 5 0 1 1-10 0a5 5 0 0 1 10 0m4.25.75a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5zM12 19a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 19m-7.75-6.25a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5zm-.03-8.53a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1-1.06 1.06l-1.5-1.5a.75.75 0 0 1 0-1.06m1.06 15.56a.75.75 0 1 1-1.06-1.06l1.5-1.5a.75.75 0 1 1 1.06 1.06zm14.5-15.56a.75.75 0 0 0-1.06 0l-1.5 1.5a.75.75 0 0 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06m-1.06 15.56a.75.75 0 1 0 1.06-1.06l-1.5-1.5a.75.75 0 1 0-1.06 1.06z"/></svg>
|
||||
<div id="theme-selector" role="region" aria-label="Theme switcher">
|
||||
<button id="btn-light" aria-label="Light mode" aria-pressed="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M12 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 2m5 10a5 5 0 1 1-10 0a5 5 0 0 1 10 0m4.25.75a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5zM12 19a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 19m-7.75-6.25a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5zm-.03-8.53a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1-1.06 1.06l-1.5-1.5a.75.75 0 0 1 0-1.06m1.06 15.56a.75.75 0 1 1-1.06-1.06l1.5-1.5a.75.75 0 1 1 1.06 1.06zm14.5-15.56a.75.75 0 0 0-1.06 0l-1.5 1.5a.75.75 0 0 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06m-1.06 15.56a.75.75 0 1 0 1.06-1.06l-1.5-1.5a.75.75 0 1 0-1.06 1.06z"/></svg>
|
||||
</button>
|
||||
<div id="theme-selector-dropdown" popover>
|
||||
<fieldset>
|
||||
<legend>Select theme:</legend>
|
||||
<div>
|
||||
<input type="radio" id="theme-system" name="theme" value="system" checked>
|
||||
<label for="theme-system">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10m0-1.5v-17a8.5 8.5 0 0 1 0 17"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="theme-dark" name="theme" value="dark">
|
||||
<label for="theme-dark">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M20.026 17.001c-2.762 4.784-8.879 6.423-13.663 3.661A10 10 0 0 1 3.13 17.68a.75.75 0 0 1 .365-1.132c3.767-1.348 5.785-2.91 6.956-5.146c1.233-2.353 1.551-4.93.689-8.463a.75.75 0 0 1 .769-.927a9.96 9.96 0 0 1 4.457 1.327c4.784 2.762 6.423 8.879 3.66 13.662"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="theme-light" name="theme" value="light">
|
||||
<label for="theme-light">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M12 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 2m5 10a5 5 0 1 1-10 0a5 5 0 0 1 10 0m4.25.75a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5zM12 19a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 19m-7.75-6.25a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5zm-.03-8.53a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1-1.06 1.06l-1.5-1.5a.75.75 0 0 1 0-1.06m1.06 15.56a.75.75 0 1 1-1.06-1.06l1.5-1.5a.75.75 0 1 1 1.06 1.06zm14.5-15.56a.75.75 0 0 0-1.06 0l-1.5 1.5a.75.75 0 0 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06m-1.06 15.56a.75.75 0 1 0 1.06-1.06l-1.5-1.5a.75.75 0 1 0-1.06 1.06z"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<button id="btn-dark" aria-label="Dark mode" aria-pressed="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M20.026 17.001c-2.762 4.784-8.879 6.423-13.663 3.661A10 10 0 0 1 3.13 17.68a.75.75 0 0 1 .365-1.132c3.767-1.348 5.785-2.91 6.956-5.146c1.233-2.353 1.551-4.93.689-8.463a.75.75 0 0 1 .769-.927a9.96 9.96 0 0 1 4.457 1.327c4.784 2.762 6.423 8.879 3.66 13.662"/></svg>
|
||||
</button>
|
||||
<span id="theme-status" class="sr-only" aria-live="polite"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue