Skip to content

Commit

Permalink
feat: theme toggle animation (#994)
Browse files Browse the repository at this point in the history
  • Loading branch information
quentinderoubaix authored Oct 31, 2024
1 parent afe3e2b commit 18b2346
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 27 deletions.
17 changes: 12 additions & 5 deletions demo/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import Versions from './menu/Versions.svelte';
import type {Snippet} from 'svelte';
import type {LayoutData} from './$types';
import viewTransition from './view-transition.css?raw';
let isMainPage = $derived($routeLevel$ === 0);
let isApi = $derived($page.route.id?.startsWith('/api/'));
Expand All @@ -29,11 +30,17 @@
});
onNavigate((navigation) => {
if (!document.startViewTransition) return;
return new Promise((resolve) => {
document.startViewTransition(async () => {
resolve();
await navigation.complete;
});
let styleElement = document.createElement('style');
styleElement.textContent = viewTransition;
document.head.appendChild(styleElement);
return new Promise<void>((resolve) => {
void document
.startViewTransition(async () => {
resolve();
await navigation.complete;
})
.ready.then(() => styleElement.remove());
});
});
Expand Down
62 changes: 53 additions & 9 deletions demo/src/routes/menu/Theme.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sun from 'bootstrap-icons/icons/sun-fill.svg?raw';
import {onMount} from 'svelte';
import type {DropdownButton} from '$lib/layout/dropdown';
import themeViewTransition from './theme-view-transition.css?raw';
interface Theme extends DropdownButton {
name: string;
Expand All @@ -23,7 +24,7 @@
name: 'Auto',
icon: halfCircle,
onclick: () => {
setTheme('auto');
void setTheme('auto');
},
isSelected: currentTheme$() === 'auto',
},
Expand All @@ -33,7 +34,7 @@
name: 'Light',
icon: sun,
onclick: () => {
setTheme('light');
void setTheme('light');
},
isSelected: currentTheme$() === 'light',
},
Expand All @@ -43,16 +44,59 @@
name: 'Dark',
icon: moon,
onclick: () => {
setTheme('dark');
void setTheme('dark');
},
isSelected: currentTheme$() === 'dark',
},
]);
function setTheme(id: string): void {
currentTheme$.set(id);
localStorage.setItem('theme', id);
applyTheme(id);
let toggle: HTMLElement;
async function setTheme(id: string, noAnimation = false) {
const themeApply = () => {
currentTheme$.set(id);
localStorage.setItem('theme', id);
applyTheme(id);
};
const appliedTheme = (theme: string) => {
if (theme === 'auto') {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'dark' : 'light';
} else {
return theme;
}
};
if (
!document.startViewTransition ||
window.matchMedia('(prefers-reduced-motion: reduce)').matches ||
noAnimation ||
appliedTheme(currentTheme$()) === appliedTheme(id)
) {
themeApply();
} else {
let styleElement = document.createElement('style');
styleElement.textContent = themeViewTransition;
document.head.appendChild(styleElement);
const viewTransition = document.startViewTransition(themeApply);
await viewTransition.ready;
const {top, left, width, height} = toggle.getBoundingClientRect();
const x = left + width / 2;
const y = top + height / 2;
const right = window.innerWidth - left;
const bottom = window.innerHeight - top;
const maxRadius = Math.hypot(Math.max(left, right), Math.max(top, bottom));
await document.documentElement.animate(
{
clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${maxRadius}px at ${x}px ${y}px)`],
},
{
duration: 1_000,
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)',
},
).finished;
await viewTransition.finished;
styleElement.remove();
}
}
function applyTheme(id: string) {
Expand All @@ -66,7 +110,7 @@
onMount(() => {
// First we search in localStorage
setTheme(localStorage.getItem('theme') ?? 'auto');
void setTheme(localStorage.getItem('theme') ?? 'auto', true);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (currentTheme$() === 'auto') {
applyTheme('auto');
Expand All @@ -75,7 +119,7 @@
});
</script>

<div class="nav-item">
<div class="nav-item" bind:this={toggle}>
<Dropdown btnClass="btn-dark-mode nav-link" ariaLabel="toggle the dark mode" items={$themes$} placement="end">
{#snippet buttonSnip()}
{#each $themes$ as theme}
Expand Down
5 changes: 5 additions & 0 deletions demo/src/routes/menu/theme-view-transition.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
::view-transition-old(*),
::view-transition-new(*) {
animation: false !important;
mix-blend-mode: normal !important;
}
13 changes: 0 additions & 13 deletions demo/src/routes/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -190,19 +190,6 @@ header {
}
}

.demo-nav-top {
view-transition-name: demo-nav-top;
}
.demo-sidebar {
view-transition-name: demo-sidebar;
}
.demo-toc {
view-transition-name: demo-toc;
}
.demo-mobile-menu {
view-transition-name: demo-mobile-menu;
}

blockquote {
@extend .border, .bg-light-subtle;
padding: 0.5rem 1.5rem;
Expand Down
12 changes: 12 additions & 0 deletions demo/src/routes/view-transition.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.demo-nav-top {
view-transition-name: demo-nav-top;
}
.demo-sidebar {
view-transition-name: demo-sidebar;
}
.demo-toc {
view-transition-name: demo-toc;
}
.demo-mobile-menu {
view-transition-name: demo-mobile-menu;
}

0 comments on commit 18b2346

Please sign in to comment.