Anatomy of the GodUI Textarea
Why the textarea'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
textarea.
To fade a ring in on the GPU, it needs its own layer whose opacity changes,
and on a <div> that layer is a ::before. A <textarea> is a replaced
element, so it gets no ::before. Wrapping it in a <div> would move your
className and ref off the element you put them on.
So the Textarea keeps shadcn's ring and drops the transition. The left field
in the scene snaps the way the Textarea does; the right one is an Input
Group, whose ::before ring fades in (slowed down here). Input Group takes a
textarea too, through InputGroupTextarea.
The result
Tab into the textarea or click it. The border and ring change in the same frame.
// 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 Textarea
Why the textarea'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
textarea.
To fade a ring in on the GPU, it needs its own layer whose opacity changes,
and on a <div> that layer is a ::before. A <textarea> is a replaced
element, so it gets no ::before. Wrapping it in a <div> would move your
className and ref off the element you put them on.
So the Textarea keeps shadcn's ring and drops the transition. The left field
in the scene snaps the way the Textarea does; the right one is an Input
Group, whose ::before ring fades in (slowed down here). Input Group takes a
textarea too, through InputGroupTextarea.
The result
Tab into the textarea or click it. The border and ring change in the same frame.
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 textarea itself, and
box-shadow can only be animated by repainting. A GPU fade needs a separate
layer whose opacity changes, and <textarea> is a replaced element, so it
can't have a ::before. A wrapper would give it one, but your className and
ref would land on the wrong element. For a ring that fades in, use
InputGroupTextarea inside Input Group.
The textarea still grows with its content (field-sizing-content); that
resize is the browser's layout, not an animation.
Why GPU-only
A box-shadow transition repaints the textarea on every frame of the fade.
GodUI's textarea 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/textarea overwrites components/ui/textarea.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].