Domain Switch

Dark Mode Toggle for React

A toggle that expands its new state as a domain across the page. A sphere grows from the switch, everything inside it changes theme or mode, and its edge is a thin seam of stars where the two worlds meet.

  • Toggle
  • Theme
  • Transition
  • Canvas

Uses motion, clsx, tailwind-merge

Inspired by Jujutsu Kaisen

Open in ChatGPTOpen in Claude
duration1.1s
collapseRatio0.80
scope.seam10px
scope.lens0.05
<DomainSwitch />

The demo follows as you drag.

New: answers in Slack
Analytics that answer back.

Ask a question in plain words. Tidal writes the query, checks it and draws the chart.

Live queries

Weekly signups, by channel

Shared boards

Three people are looking at Q3

Take this demo to your coding agent.
Add this moment from KeyframeUI to our project and adapt it to our content.

Domain Switch: A toggle that expands its new state as a domain across the page. A sphere grows from the switch, everything inside it changes theme or mode, and its edge is a thin seam of stars where the two worlds meet.
This demo, Dark Mode: A landing page's theme toggle. Dark mode opens from the button as a domain and takes the page over, with a seam of stars at its edge; press again to draw it back in.

1. Install it:
   npx shadcn@latest add @keyframeui/domain-switch-demo
   (If the CLI can't find @keyframeui, use https://keyframeui.com/r/domain-switch-demo.json instead.)
   It adds the demo, components/domain-switch-demo.tsx, and the component, components/ui/domain-switch.tsx.

2. Make it ours: our copy, data, images and colors. Keep the structure the motion depends on. If we only need the effect, use <DomainSwitch> on its own.

3. These props set its feel. Change them before you change the file:
   - duration (default 1.1): Seconds the domain takes to grow across the scope. Collapsing takes a little less.
   - collapseRatio (default 0.8): How long collapsing takes, as a share of `duration`. `1` draws the domain back in as slowly as it grew; lower snaps it shut.
   - seam on <DomainSwitchScope> (default 10): Width of the seam of stars at the domain's edge, in px. `0` gives a clean edge.
   - lens on <DomainSwitchScope> (default 0.05): How much the inside is magnified while the domain is small. It relaxes to none as the domain grows.

4. Don't change, in components/ui/domain-switch.tsx:
   - Every frame is written straight to the DOM from one motion value, `progress`: the inside's `clip-path` circle, the lens scale and the canvas seam. Nothing in a frame re-renders React.
   - Both worlds are real markup stacked in one grid cell, the inside clipped on top. The world not in charge is `inert` and `aria-hidden`, and focus moves to the same toggle in the other world.
   - A toggle mid-flight turns around from the current progress, never from the start; the move scales with the square root of the distance left and never drops under `TIMING.shortest`.
   - The sphere opens from the pressed toggle, measured in the unmagnified outside world, and grows past the farthest corner by the seam's width so the seam leaves with it.
   - Reduced motion drops the clip, lens and seam and crossfades the inside world's opacity.
   Its timing is all in the TIMING block at the top of that file.

Docs: https://keyframeui.com/components/domain-switch.md

Installation

Add the component on its own with the CLI, or copy the file in by hand.

The CLI writes components/ui/domain-switch.tsx and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.

npx shadcn@latest add @keyframeui/domain-switch

Examples

Finished sections built with Domain Switch. Each one installs whole, with the component included.

Focus Mode

A writing app. Focus opens a calm, dark domain in which the sidebar and toolbar, wrapped in `DomainSwitchAside`, dissolve, and the document in `DomainSwitchMain` comes forward. One `Editor` renders both worlds. Esc leaves.

BIUH1

Essay · 412 words

On slow software

The best tools I use don't ask for my attention. They wait. A text editor that opens before I've finished the thought, a calendar that doesn't blink.

Most software is built to be noticed. Badges, banners, a tour on every release. Each one is small, and together they are the reason I can't finish a paragraph

Take this demo to your coding agent.
Add this moment from KeyframeUI to our project and adapt it to our content.

Domain Switch: A toggle that expands its new state as a domain across the page. A sphere grows from the switch, everything inside it changes theme or mode, and its edge is a thin seam of stars where the two worlds meet.
This demo, Focus Mode: A writing app. Focus opens a calm, dark domain in which the sidebar and toolbar, wrapped in `DomainSwitchAside`, dissolve, and the document in `DomainSwitchMain` comes forward. One `Editor` renders both worlds. Esc leaves.

1. Install it:
   npx shadcn@latest add @keyframeui/domain-switch-focus-demo
   (If the CLI can't find @keyframeui, use https://keyframeui.com/r/domain-switch-focus-demo.json instead.)
   It adds the demo, components/domain-switch-focus-demo.tsx, and the component, components/ui/domain-switch.tsx.

2. Make it ours: our copy, data, images and colors. Keep the structure the motion depends on. If we only need the effect, use <DomainSwitch> on its own.

3. These props set its feel. Change them before you change the file:
   - duration (default 1.1): Seconds the domain takes to grow across the scope. Collapsing takes a little less.
   - collapseRatio (default 0.8): How long collapsing takes, as a share of `duration`. `1` draws the domain back in as slowly as it grew; lower snaps it shut.
   - seam on <DomainSwitchScope> (default 10): Width of the seam of stars at the domain's edge, in px. `0` gives a clean edge.
   - lens on <DomainSwitchScope> (default 0.05): How much the inside is magnified while the domain is small. It relaxes to none as the domain grows.

4. Don't change, in components/ui/domain-switch.tsx:
   - Every frame is written straight to the DOM from one motion value, `progress`: the inside's `clip-path` circle, the lens scale and the canvas seam. Nothing in a frame re-renders React.
   - Both worlds are real markup stacked in one grid cell, the inside clipped on top. The world not in charge is `inert` and `aria-hidden`, and focus moves to the same toggle in the other world.
   - A toggle mid-flight turns around from the current progress, never from the start; the move scales with the square root of the distance left and never drops under `TIMING.shortest`.
   - The sphere opens from the pressed toggle, measured in the unmagnified outside world, and grows past the farthest corner by the seam's width so the seam leaves with it.
   - Reduced motion drops the clip, lens and seam and crossfades the inside world's opacity.
   Its timing is all in the TIMING block at the top of that file.

Docs: https://keyframeui.com/components/domain-switch.md

Command Palette

Press ⌘K (or the search icon) and a domain opens from the icon in which the page becomes a command palette. Run a command or press Esc and it collapses back into the icon.

Deployments

Press ⌘K for commands

  • Cache edge responses4m
  • Fuzzy match on titles38m
  • Bump runtime to 222h
  • Fallback social card5h
  • Drop legacy routes1d
Take this demo to your coding agent.
Add this moment from KeyframeUI to our project and adapt it to our content.

Domain Switch: A toggle that expands its new state as a domain across the page. A sphere grows from the switch, everything inside it changes theme or mode, and its edge is a thin seam of stars where the two worlds meet.
This demo, Command Palette: Press ⌘K (or the search icon) and a domain opens from the icon in which the page becomes a command palette. Run a command or press Esc and it collapses back into the icon.

1. Install it:
   npx shadcn@latest add @keyframeui/domain-switch-command-demo
   (If the CLI can't find @keyframeui, use https://keyframeui.com/r/domain-switch-command-demo.json instead.)
   It adds the demo, components/domain-switch-command-demo.tsx, and the component, components/ui/domain-switch.tsx.

2. Make it ours: our copy, data, images and colors. Keep the structure the motion depends on. If we only need the effect, use <DomainSwitch> on its own.

3. These props set its feel. Change them before you change the file:
   - duration (default 1.1): Seconds the domain takes to grow across the scope. Collapsing takes a little less.
   - collapseRatio (default 0.8): How long collapsing takes, as a share of `duration`. `1` draws the domain back in as slowly as it grew; lower snaps it shut.
   - seam on <DomainSwitchScope> (default 10): Width of the seam of stars at the domain's edge, in px. `0` gives a clean edge.
   - lens on <DomainSwitchScope> (default 0.05): How much the inside is magnified while the domain is small. It relaxes to none as the domain grows.

4. Don't change, in components/ui/domain-switch.tsx:
   - Every frame is written straight to the DOM from one motion value, `progress`: the inside's `clip-path` circle, the lens scale and the canvas seam. Nothing in a frame re-renders React.
   - Both worlds are real markup stacked in one grid cell, the inside clipped on top. The world not in charge is `inert` and `aria-hidden`, and focus moves to the same toggle in the other world.
   - A toggle mid-flight turns around from the current progress, never from the start; the move scales with the square root of the distance left and never drops under `TIMING.shortest`.
   - The sphere opens from the pressed toggle, measured in the unmagnified outside world, and grows past the farthest corner by the seam's width so the seam leaves with it.
   - Reduced motion drops the clip, lens and seam and crossfades the inside world's opacity.
   Its timing is all in the TIMING block at the top of that file.

Docs: https://keyframeui.com/components/domain-switch.md

Props

checked

Typeboolean

Whether the inside world has taken over. Control it with onCheckedChange.

defaultChecked

TypebooleanDefaultfalse

The starting state when it isn't controlled.

onCheckedChange

Type(checked: boolean) => void

Called the moment a toggle is pressed.

onChange

Type(inside: boolean) => void

Called once the domain has finished growing or collapsing, with the world that now covers the scope. A good place to persist a theme or apply it to the rest of the app. Not called when a toggle turns it around mid-flight.

duration

TypenumberDefault1.1

Seconds the domain takes to grow across the scope. Collapsing takes a little less.

collapseRatio

TypenumberDefault0.8

How long collapsing takes, as a share of duration. 1 draws the domain back in as slowly as it grew; lower snaps it shut.

toggle.children

TypeReact.ReactNode

DomainSwitchToggle: the button's content. It's a real switch (role="switch", aria-checked, Space and Enter). Put one outside the scope, or one in each world at the same spot, and the domain opens from its center.

toggle.className

Typestring

DomainSwitchToggle: classes for the button.

scope.inside

TypeReact.ReactNode

DomainSwitchScope: the world the domain brings in.

scope.outside

TypeReact.ReactNode

DomainSwitchScope: the world it takes over.

scope.children

TypeReact.ReactNode

DomainSwitchScope: rendered in whichever world inside or outside leaves out. One page can serve both by reading useDomainSwitch().world or using DomainSwitchAside and DomainSwitchMain.

scope.seam

TypenumberDefault10

DomainSwitchScope: width of the seam of stars at the domain's edge, in px. 0 gives a clean edge.

scope.lens

TypenumberDefault0.05

DomainSwitchScope: how much the inside is magnified while the domain is small. It relaxes to none as the domain grows.

scope.seamClassName

TypestringDefault"text-zinc-950"

DomainSwitchScope: classes that set the seam's color (a text-* color). Its stars take the opposite shade.

scope.className

Typestring

DomainSwitchScope: classes for the area the domain covers, which sizes both worlds. It clips them, so give it the rounded corners.

aside.insideClassName

TypestringDefault"scale-[0.98] opacity-[0.14] blur-[5px] saturate-50"

DomainSwitchAside: wraps something non-essential. Inside the domain it takes these classes and can't be reached; outside it's left alone.

main.insideClassName

TypestringDefault"scale-[1.04]"

DomainSwitchMain: wraps the content that matters. Inside the domain it takes these classes.