Anatomy of the GodUI Input
Why the input's focus ring snaps instead of fading, and where to get a ring that fades in on the GPU.
↓Scroll to step through it
Why the ring snaps
shadcn draws the focus ring as a 3px box-shadow and transitions it. A
shadow can't move to the compositor: every frame of that fade repaints the
field.
The GPU way to fade something in is to give it its own layer and change that
layer's opacity. On a <div> that layer is a ::before. An <input> is a
replaced element, though, and replaced elements don't get ::before or
::after. The other option, wrapping the input in a <div>, would move your
className, ref and layout classes off the element you put them on.
So the Input keeps shadcn's ring and drops the transition. In the scene, the
field on the left is a real Input; its ring snaps on and off. The one on the
right is an Input Group, whose ring sits on a ::before and fades in (slowed
down here).
The result
Tab into the field or click it. The border and ring change in the same frame. For a ring that fades in, wrap the field in Input Group.
// shadcn"… transition-[color,box-shadow] …focus-visible:ring-[3px] focus-visible:ring-ring/50"// GodUI: same ring, no transition"… focus-visible:border-ringfocus-visible:ring-[3px] focus-visible:ring-ring/50"Anatomy of the GodUI Input
Why the input's focus ring snaps instead of fading, and where to get a ring that fades in on the GPU.
// shadcn"… transition-[color,box-shadow] …focus-visible:ring-[3px] focus-visible:ring-ring/50"// GodUI: same ring, no transition"… focus-visible:border-ringfocus-visible:ring-[3px] focus-visible:ring-ring/50"Why the ring snaps
shadcn draws the focus ring as a 3px box-shadow and transitions it. A
shadow can't move to the compositor: every frame of that fade repaints the
field.
The GPU way to fade something in is to give it its own layer and change that
layer's opacity. On a <div> that layer is a ::before. An <input> is a
replaced element, though, and replaced elements don't get ::before or
::after. The other option, wrapping the input in a <div>, would move your
className, ref and layout classes off the element you put them on.
So the Input keeps shadcn's ring and drops the transition. In the scene, the
field on the left is a real Input; its ring snaps on and off. The one on the
right is an Input Group, whose ring sits on a ::before and fades in (slowed
down here).
The result
Tab into the field or click it. The border and ring change in the same frame. For a ring that fades in, wrap the field in Input Group.
What's animated
| Interaction | Keyframe / mechanism | Properties | Easing | Duration |
|---|---|---|---|---|
| Focus ring | snaps (box-shadow, no transition) | none | none | none |
| Border, invalid ring | snap | none | none | none |
Why it snaps: the ring is a box-shadow on the input itself, and
box-shadow can only be animated by repainting. A GPU fade needs a separate
layer whose opacity changes, and <input> is a replaced element, so it
can't have a ::before. A wrapper <div> would give it one, but then
className, ref and every layout class at your call sites would land on the
wrong element. The snap keeps the input a drop-in replacement. When you want the ring
to fade in, wrap the field in Input Group.
Why GPU-only
A box-shadow transition repaints the input on every frame of the fade.
GodUI's input never animates, so focusing it costs one repaint.
Reduced motion
Nothing to reduce: the ring already appears in a single frame.
Replacing shadcn
npx shadcn add @godui/input overwrites components/ui/input.tsx. Exports,
props and the data-slot attribute match shadcn/ui new-york-v4, so existing
imports keep working. The only class removed is transition-[color,box-shadow].