Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 48 additions & 6 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

:root {
color-scheme: light;
--theme-transition-duration: 420ms;
--theme-transition-ease: cubic-bezier(0.22, 1, 0.36, 1);
--background: 210 40% 98%;
--foreground: 222 47% 11%;
--card: 0 0% 100%;
Expand Down Expand Up @@ -52,19 +54,48 @@
body {
@apply bg-background text-foreground antialiased;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
background-color: hsl(var(--background));
min-height: 100vh;
position: relative;
isolation: isolate;
}

body::before,
body::after {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
transition: opacity calc(var(--theme-transition-duration) + 120ms) var(--theme-transition-ease);
}

body::before {
background-image:
radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.08), transparent 35%),
radial-gradient(circle at 80% 0%, rgba(168, 85, 247, 0.08), transparent 30%),
linear-gradient(180deg, #f8fbff 0%, #f2f5f9 40%, #f9fafb 100%);
min-height: 100vh;
opacity: 1;
}

.dark body {
@apply bg-background text-foreground;
body::after {
background-image:
radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.08), transparent 35%),
radial-gradient(circle at 80% 0%, rgba(124, 58, 237, 0.12), transparent 30%),
linear-gradient(180deg, #0f172a 0%, #0b1221 40%, #0a0f1c 100%);
opacity: 0;
}

.dark body {
@apply bg-background text-foreground;
}

.dark body::before {
opacity: 0;
}

.dark body::after {
opacity: 1;
}

.card {
Expand All @@ -83,9 +114,15 @@ html.theme-transition,
html.theme-transition *,
html.theme-transition *::before,
html.theme-transition *::after {
transition-property: background-color, border-color, color, fill, stroke, box-shadow;
transition-duration: 460ms;
transition-timing-function: cubic-bezier(0.32, 0, 0.2, 1);
transition-property: background-color, border-color, color, fill, stroke, box-shadow, opacity, backdrop-filter;
transition-duration: var(--theme-transition-duration);
transition-timing-function: var(--theme-transition-ease);
}

::view-transition-old(root),
::view-transition-new(root) {
animation-duration: var(--theme-transition-duration);
animation-timing-function: var(--theme-transition-ease);
}

@media (prefers-reduced-motion: reduce) {
Expand All @@ -95,4 +132,9 @@ html.theme-transition *::after {
html.theme-transition *::after {
transition: none !important;
}

::view-transition-old(root),
::view-transition-new(root) {
animation: none !important;
}
}
41 changes: 39 additions & 2 deletions components/theme-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,59 @@ import { GithubLink } from "./github-link";

const emptySubscribe = () => () => { };

type ViewTransitionDocument = Document & {
startViewTransition?: (callback: () => void) => { finished: Promise<void> };
};

export function ThemeToggle() {
const { t } = useTranslation();
const { theme, setTheme, resolvedTheme } = useTheme();
const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);
const THEME_TRANSITION_MS = 520;
const getThemeTransitionMs = () => {
if (typeof window === "undefined") {
return 420;
}

const value = window
.getComputedStyle(document.documentElement)
.getPropertyValue("--theme-transition-duration")
.trim();

if (!value) {
return 420;
}

const numeric = Number.parseFloat(value);
if (!Number.isFinite(numeric)) {
return 420;
}

return value.endsWith("ms") ? Math.round(numeric) + 60 : Math.round(numeric * 1000) + 60;
};

const current = resolvedTheme || theme || "light";
const next = current === "light" ? "dark" : "light";

const handleToggle = () => {
const prefersReducedMotion =
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;

const doc = document as ViewTransitionDocument;
if (doc.startViewTransition && !prefersReducedMotion) {
doc.startViewTransition(() => {
setTheme(next);
});
return;
}

if (typeof document !== "undefined") {
const root = document.documentElement;
const transitionMs = getThemeTransitionMs();
root.classList.add("theme-transition");
window.setTimeout(() => {
root.classList.remove("theme-transition");
}, THEME_TRANSITION_MS);
}, transitionMs);
}

requestAnimationFrame(() => {
Expand Down
Loading