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.
↓Scroll to step through it
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.
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.
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.
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.
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.
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.
<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)));}}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.
<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)));}}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.
// 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" />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.
// 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; }}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.
// 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;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.
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);}}/>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.
By clicking this checkbox, you agree to the terms and conditions.
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
| Interaction | Keyframe / mechanism | Properties | Easing | Duration |
|---|---|---|---|---|
| Check (fill) | godui-checkbox-fill-in: a disc grows from a dot, squared off by the box's clip | scale 0 → 1.42 | ease-spring-smooth | 260ms |
| Check (mark) | godui-checkbox-draw + godui-checkbox-ink: a clip window slides in while the mark slides back | translate | ease-spring-smooth | 190ms, after 70ms |
| Check (box) | godui-pop | scale 1 → 0.9 → 1 | ease-spring-bouncy | 260ms |
| Uncheck (mark) | godui-checkbox-check-out | opacity, scale 1 → 0.7 | ease-out-expo | 150ms |
| Uncheck (fill) | godui-checkbox-fill-out: the disc shrinks back to a dot, in the checked colors | scale 1.42 → 0 | ease-spring-smooth | 150ms |
| Border | snap | none | none | none |
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.