All Taste components
Animated screens
Dark mode switch
A settings screen where turning on dark appearance sends a circle across it that flips the colours.
switch toggle · circular ripple · light ↔ dark
Settings
PR
Priya Raman
Account, security, devices
Display
Dark appearance
Text sizeDefault
App iconClassic
General
NotificationsOn
Privacy
LanguageEnglish
/* Needs the Taste globals.css: light on :root, dark on .dark.
The screen is drawn twice; the dark copy sits on top, clipped to a circle
that grows out of the switch — so the flip stays inside this screen. */
"use client";
import { useRef, useState } from "react";
import { Bell, ChevronRight, Globe, LayoutGrid, Lock, Moon, Type } from "lucide-react";
import { cn } from "@/lib/utils";
const display = [
{ label: "Text size", value: "Default", icon: Type, tone: "bg-primary" },
{ label: "App icon", value: "Classic", icon: LayoutGrid, tone: "bg-warning" },
];
const general = [
{ label: "Notifications", value: "On", icon: Bell, tone: "bg-danger" },
{ label: "Privacy", value: "", icon: Lock, tone: "bg-success" },
{ label: "Language", value: "English", icon: Globe, tone: "bg-primary" },
];
function Row({ icon: Icon, tone, label, children }: { icon: typeof Bell; tone: string; label: string; children: React.ReactNode }) {
return (
<div className="flex items-center gap-3 pl-3 [&+&>div]:border-t [&+&>div]:border-border">
<span className={cn("grid size-[30px] shrink-0 place-items-center rounded-md text-bg", tone)}><Icon className="size-4" aria-hidden /></span>
<div className="flex min-h-12 flex-1 items-center gap-2 pr-3 text-[14.5px]"><span className="flex-1">{label}</span>{children}</div>
</div>
);
}
function Settings({ dark, onToggle, switchRef }: { dark: boolean; onToggle?: () => void; switchRef?: React.Ref<HTMLButtonElement> }) {
const value = (s: string) => <span className="inline-flex items-center gap-0.5 text-[13.5px] text-fg-subtle">{s}<ChevronRight className="size-4" /></span>;
return (
<div className="min-h-full bg-surface-2 px-4 pb-8 pt-12 text-fg">
<h1 className="text-2xl font-semibold tracking-tight">Settings</h1>
<div className="mt-3 flex items-center gap-3 rounded-lg bg-bg p-3">
<span className="grid size-9 place-items-center rounded-full bg-primary-soft text-[13px] font-medium text-primary-soft-fg">PR</span>
<div className="flex-1"><p className="text-sm font-semibold">Priya Raman</p><p className="text-xs text-fg-muted">Account, security, devices</p></div>
<ChevronRight className="size-4 text-fg-subtle" />
</div>
<p className="mx-1 mb-1.5 mt-5 text-xs font-medium text-fg-muted">Display</p>
<div className="overflow-hidden rounded-lg bg-bg">
<Row icon={Moon} tone="bg-info" label="Dark appearance">
<button ref={switchRef} type="button" role="switch" aria-checked={dark} aria-label="Dark appearance" onClick={onToggle} tabIndex={onToggle ? 0 : -1}
className={cn("relative h-[26px] w-11 rounded-full transition-colors duration-300", dark ? "bg-primary" : "bg-border-strong")}>
<span className={cn("absolute left-0.5 top-0.5 size-[22px] rounded-full bg-neutral-50 shadow transition-transform duration-300 ease-spring", dark && "translate-x-[18px]")} />
</button>
</Row>
{display.map((r) => <Row key={r.label} {...r}>{value(r.value)}</Row>)}
</div>
<p className="mx-1 mb-1.5 mt-5 text-xs font-medium text-fg-muted">General</p>
<div className="overflow-hidden rounded-lg bg-bg">{general.map((r) => <Row key={r.label} {...r}>{value(r.value)}</Row>)}</div>
</div>
);
}
export default function AppearanceSettingsScreen() {
const [dark, setDark] = useState(false);
const [origin, setOrigin] = useState({ x: 0, y: 0 });
const root = useRef<HTMLDivElement>(null);
const sw = useRef<HTMLButtonElement>(null);
function toggle() {
const box = root.current?.getBoundingClientRect(), s = sw.current?.getBoundingClientRect();
if (box && s) setOrigin({ x: s.left - box.left + s.width / 2, y: s.top - box.top + s.height / 2 });
setDark((d) => !d);
}
const radius = dark ? "150%" : "0px";
return (
<div ref={root} className="relative mx-auto h-dvh max-w-md overflow-hidden font-sans">
<div className="absolute inset-0" inert={dark}><Settings dark={dark} onToggle={toggle} switchRef={sw} /></div>
{/* The dark copy (class "dark" swaps the Taste variables), revealed by a growing circle. */}
<div className="dark absolute inset-0 transition-[clip-path] duration-700 ease-taste motion-reduce:transition-none" inert={!dark}
style={{ clipPath: "circle(" + radius + " at " + origin.x + "px " + origin.y + "px)" }}>
<Settings dark={dark} onToggle={toggle} />
</div>
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.