GodUIGodUI
129Follow on X

Anatomy of the GodUI Checkbox

How the GodUI Checkbox floods with ink from a dot, draws its check, and drains again, animating only scale, translate and opacity, in whatever colors the box is given.

01/06Ink from a dot
tsx
<span className="size-full scale-[1.42] rounded-full bg-inherit… :animate-godui-checkbox-fill-in" />@keyframes godui-checkbox-fill-in {from {  opacity: var(--godui-motion);  scale: calc(1.42 * (1 - var(--godui-motion)));}}
01

Ink from a dot

Checking the box grows a dot from the middle until it fills the box.

The dot is a circle exactly as wide as the box's diagonal, which is why it rests at scale: 1.42. The box clips it to its own rounded corners, so at rest you only see a filled box. Grow the circle from scale: 0 and the clip does the shape change: a dot, then a disc whose edge reaches the sides, then the corners fill in. A circle turns into a rounded square with a single scale.

02/06Whose color is it?
tsx
// Root: shadcn's (or your) checked colors, but the fill isn't painted."data-[state=checked]:bg-primary … not-data-[state=unchecked]:bg-clip-text"// Indicator and disc pick the color up instead.<CheckboxPrimitive.Indicator className="bg-inherit bg-clip-text …"><span className="… bg-inherit" />
02

Whose color is it?

The disc must be the box's checked color: shadcn's bg-primary, or whatever your own data-[state=checked]:bg-* class says. So GodUI leaves those classes on the box and changes where the color is painted.

While checked, the box clips its background to its text (background-clip: text). A checkbox has no text, so the box paints no fill at all. The indicator inherits the color the same way, without painting it, and the disc inherits it from the indicator. The disc draws your colors.

03/06The check is drawn
tsx
// The window slides in from the left…@keyframes godui-checkbox-draw {from { translate: calc(-100% * var(--godui-motion)) 0; }}// …and the mark inside it slides back by as much.@keyframes godui-checkbox-ink {from { translate: calc(100% * var(--godui-motion)) 0; }}
03

The check is drawn

Drawing a check stroke by stroke would mean animating stroke-dashoffset, which repaints the SVG on every frame. GodUI draws it with a clip instead.

The check sits in an overflow-hidden window. The window slides in from one width to the left while the mark inside slides back from one width to the right, on the same clock. The two cancel, so the stroke never moves; only the window's right edge does, uncovering it left to right, the direction a check is drawn. It starts a beat after the disc and ends with it.

04/06Draining
tsx
// In the capture phase, before Radix unchecks the box:onClickCapture={() => readInk()}// Once it's unchecked, keep the old colors on the exiting indicator.el.style.color = ink.current.color;el.style.backgroundColor = ink.current.background;
04

Draining

Unchecking runs it backwards, faster: the mark fades and the disc shrinks to a dot. But once the box is unchecked, its colors are the unchecked ones, and the disc inherits them. A disc that inherits a transparent fill can't be seen shrinking.

So the checkbox reads its checked colors while it still has them, on the click's way down before Radix toggles anything, and pins them on the indicator while it leaves. A near-silent hold keyframe keeps Radix from removing the indicator until the disc is gone.

05/06Never on page load
tsx
const [animate, setAnimate] = React.useState(false);const [lastChecked, setLastChecked] = React.useState(checked);if (checked !== lastChecked) {setLastChecked(checked);setAnimate(true);}<CheckboxPrimitive.Rootdata-animate={animate || undefined}onCheckedChange={(value) => {  setAnimate(true);  onCheckedChange?.(value);}}/>
05

Never on page load

A settings page that loads with ten boxes already checked shouldn't flood ten times. So the checkbox only animates after its state changes.

It starts with data-animate unset, which turns every keyframe off. The first change sets it: a click or Space through onCheckedChange, or a controlled checked prop compared against the last value during render. From then on, every check floods and draws, and every uncheck drains.

06/06Result

By clicking this checkbox, you agree to the terms and conditions.

06

The result

Click the boxes or their labels, or Tab to one and press Space. The box that loads checked stays still until you change it.

What's animated

InteractionKeyframe / mechanismPropertiesEasingDuration
Check (fill)godui-checkbox-fill-in: a disc grows from a dot, squared off by the box's clipscale 0 → 1.42ease-spring-smooth260ms
Check (mark)godui-checkbox-draw + godui-checkbox-ink: a clip window slides in while the mark slides backtranslateease-spring-smooth190ms, after 70ms
Check (box)godui-popscale 1 → 0.9 → 1ease-spring-bouncy260ms
Uncheck (mark)godui-checkbox-check-outopacity, scale 1 → 0.7ease-out-expo150ms
Uncheck (fill)godui-checkbox-fill-out: the disc shrinks back to a dot, in the checked colorsscale 1.42 → 0ease-spring-smooth150ms
Bordersnapnonenonenone

Animations run only after the checked state changes, by a click, the keyboard or a controlled checked prop, never on the first render.

Why GPU-only

The disc animates scale, the check translate, scale and opacity, and the box dips on scale. The border changes color in one step; on a 16px box the ink says "checked" before a color tween could. shadcn's checkbox transitions the focus ring's box-shadow; GodUI's doesn't.

Reduced motion

--godui-motion: 0 cancels the growth, the slide and the dip: the disc and the check fade in, and fade out again (150ms).

Replacing shadcn

npx shadcn add @godui/checkbox overwrites components/ui/checkbox.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.