Skip to content

No.90 · Controls· New

Theme Shift

A light/dark switch where the change of theme is the show, in seven versions: the new theme blooms out of the switch across the page, the old one shatters into falling shards, spreads in like ink, peels off like a sticker, shuts like blinds, dissolves into pixels, or melts away in drips. Works in every browser and stays inside its own box.

The prompt

Copy this text

Automatic copying is unavailable. Copy the selected text using your device’s copy command.

Written for the variant you picked above: Bloom

Build a light/dark theme switch where the transition between themes is the point. Two parts: ThemeStage (props: theme, onThemeChange, transition, motion, className, style, children) wraps the page or app, and ThemeSwitch (props: label, className) is the control, placed anywhere inside it — here in the site's navbar. A useThemeShift() hook gives other controls { theme, shift(next, fromPoint) }. The page's look follows data-theme on a wrapper (CSS custom properties per theme). Don't use document.startViewTransition: it animates the whole document (it would sweep a whole gallery from one card) and gives a single snapshot layer. Instead, on a switch: (1) capture — cloneNode(true) the page wrapper, strip every id, mark it inert and aria-hidden, copy canvas pixels with drawImage, copy form values, and record scroll positions; keep the old data-theme so the copy renders the old theme. (2) Measure only the part of the stage on screen (the stage's rect intersected with the viewport, divided by the stage's scale so it works inside a transformed preview), and lay copies over exactly that box in an absolutely positioned, pointer-events-none, overflow-hidden host inside the stage (perspective 1400px), each copy offset so its content lines up with the live page. (3) Flip the live page's theme with flushSync, so it changes before the next paint and only the copy is ever seen in the old theme. (4) Take the copy away with the chosen transition, driven by requestAnimationFrame from progress 0 to 1, then remove every layer. The origin of each transition is the centre of the switch, converted into the box's coordinates. Clip shapes use clip-path: path() (rectangles, circles and polygons built as path data, evenodd where a hole is needed), which stays crisp and needs no mask images. A second press during a transition finishes the current one first. Reduced motion (or motion="reduced") switches instantly. A page with more than 1,500 elements gets fewer copies. The switch is a 38px round role="switch" button (aria-checked = dark, label "Dark theme"), with a hairline border and the page's surface colour behind it. Inside, a sun: a 4.6px disc and 8 short rays. Going dark, the rays rotate 90° and shrink to 30% as they fade, the disc swells to 1.55×, and a 5.2px disc in the switch's own background colour slides in from the top right (translate 9px,−9px → 3.8px,−3.8px, scale 0.4 → 1) to bite it into a crescent — 520ms cubic-bezier(0.65,0,0.35,1). Draw the bite as a disc, not an SVG mask: the copy has no ids, so a mask would stop working in it. Demo: a small studio website — navbar (logo, four links, the switch), a hero with a serif headline, two buttons and gradient-drawn hills under a sun, three feature cards with CSS-drawn art, a stats strip and a footer — in a warm paper light theme (#f5f2ec, text #17140f, accent #c2410c) and a deep night theme (#0d0f13, text #ebe8e2, accent #f59e0b). It switches by itself 1.4s after opening and then every 3.4s, starting from the switch, until the visitor touches it. Selected variant — Bloom (bloom): Bloom: the new theme grows as a circle out of the switch until it reaches the farthest corner, over 820ms with cubic-bezier(0.65, 0, 0.35, 1). One copy of the old theme, clipped with path(evenodd) to the whole box minus a circle of that radius. This selection takes precedence over references to other themes or options in the general brief. Implement this selected appearance; do not add a variant picker or alternative appearances unless requested. Implementation requirements: Use React and TypeScript. Keep the component self-contained, with uniquely scoped CSS and no animation libraries. Include required imports, styles and font setup in the result. Interaction: Press the switch (or Space or Enter on it) to change the theme; the transition starts from the switch. The demo switches by itself until you touch it. Motion: Each transition lasts 0.8–1.4s and runs on requestAnimationFrame over a still copy of the old theme; the switch morphs over 520ms. Reduced motion switches instantly. Accessibility: The switch is a real button with role switch, aria-checked and a label, reachable by keyboard with a visible focus ring. The copies shown during a transition are inert, hidden from assistive tech and carry no ids, so focus and the accessibility tree only ever see the live page. Responsive behavior: Works at any size: only the visible part of the stage is animated, so long pages cost the same as short ones. The demo site stacks on narrow screens. Preserve keyboard access, visible focus and reduced-motion behavior. Keep fictional demo data and simulated service calls in a separate usage example; expose callbacks for real actions and recover from rejected requests. Keep usage-example links inside the demo: use local buttons or prevent navigation. Demonstration links must not navigate to Vitrine routes or open other pages; preserve real URL support in the reusable component itself. Verify the result at narrow and wide viewport sizes, with keyboard input and reduced motion enabled.

Notes

Interaction
Press the switch (or Space or Enter on it) to change the theme; the transition starts from the switch. The demo switches by itself until you touch it.
Motion
Each transition lasts 0.8–1.4s and runs on requestAnimationFrame over a still copy of the old theme; the switch morphs over 520ms. Reduced motion switches instantly.
Accessibility
The switch is a real button with role switch, aria-checked and a label, reachable by keyboard with a visible focus ring. The copies shown during a transition are inert, hidden from assistive tech and carry no ids, so focus and the accessibility tree only ever see the live page.
Responsive
Works at any size: only the visible part of the stage is animated, so long pages cost the same as short ones. The demo site stacks on narrow screens.