Anatomy of the GodUI Switch
A thumb on a spring, and a pseudo-element layer that turns the track's background transition into an opacity one, so the color change never repaints.
↓Scroll to step through it
The thumb
The thumb's position comes from shadcn's own classes:
data-[state=checked]:translate-x-[calc(100%-2px)]. Tailwind v4 compiles that
to the individual translate property, so GodUI transitions exactly that
property, for 260ms on ease-spring-snappy.
The snappy spring barely overshoots. A bouncier curve would push the thumb past the end of the track before it settles.
A color change without painting
shadcn switches the track from bg-input to bg-primary with
transition-all, so the browser repaints the track on every frame of the
color change.
GodUI keeps the track at bg-input and draws the checked color on a
::before layer that covers it. Toggling only changes that layer's
opacity, which the compositor can blend without repainting anything. The
layer uses -inset-px so it also covers the track's transparent 1px border,
and the thumb is relative so it paints above the layer.
The result
Click the switch or its label, or focus it and press Space.
<SwitchPrimitive.Thumbdata-slot="switch-thumb"className={cn( "pointer-events-none relative block rounded-full bg-background ring-0 transition-[translate] duration-(--godui-duration-base) ease-spring-snappy motion-reduce:transition-none … data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 …")}/>Anatomy of the GodUI Switch
A thumb on a spring, and a pseudo-element layer that turns the track's background transition into an opacity one, so the color change never repaints.
<SwitchPrimitive.Thumbdata-slot="switch-thumb"className={cn( "pointer-events-none relative block rounded-full bg-background ring-0 transition-[translate] duration-(--godui-duration-base) ease-spring-snappy motion-reduce:transition-none … data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 …")}/>The thumb
The thumb's position comes from shadcn's own classes:
data-[state=checked]:translate-x-[calc(100%-2px)]. Tailwind v4 compiles that
to the individual translate property, so GodUI transitions exactly that
property, for 260ms on ease-spring-snappy.
The snappy spring barely overshoots. A bouncier curve would push the thumb past the end of the track before it settles.
// shadcn/ui"… transition-all … data-[state=checked]:bg-primary data-[state=unchecked]:bg-input"// GodUI"relative … bg-input","before:pointer-events-none before:absolute before:-inset-px before:rounded-fullbefore:bg-primary before:opacity-0 before:transition-opacitybefore:duration-(--godui-duration-fast) data-[state=checked]:before:opacity-100"A color change without painting
shadcn switches the track from bg-input to bg-primary with
transition-all, so the browser repaints the track on every frame of the
color change.
GodUI keeps the track at bg-input and draws the checked color on a
::before layer that covers it. Toggling only changes that layer's
opacity, which the compositor can blend without repainting anything. The
layer uses -inset-px so it also covers the track's transparent 1px border,
and the thumb is relative so it paints above the layer.
The result
Click the switch or its label, or focus it and press Space.
What's animated
| Interaction | Keyframe / mechanism | Properties | Easing | Duration |
|---|---|---|---|---|
| Toggle (thumb) | transition | translate | ease-spring-snappy | 260ms |
| Toggle (track color) | transition on a ::before layer | opacity | ease | 150ms |
The root's className is merged after GodUI's classes as usual. If you set a
custom checked color with data-[state=checked]:bg-*, set it on the layer
instead: data-[state=checked]:before:bg-*.
Why GPU-only
The thumb animates translate and the color layer animates opacity. shadcn's
transition-all animates background-color and the focus ring's
box-shadow, which repaint every frame.
Reduced motion
motion-reduce:transition-none on the thumb makes it move instantly. The color
layer still fades for 150ms, since a fade isn't motion.
Replacing shadcn
npx shadcn add @godui/switch overwrites components/ui/switch.tsx. Exports,
props and data-slot attributes match shadcn/ui new-york-v4, so existing
imports keep working. It also installs godui-motion (easings, keyframes, the
FLIP hook) into your project.