GodUIGodUI
129Follow on X

Anatomy of the GodUI Input Group

A focus ring that fades and grows in on a pseudo-element, lands exactly where shadcn's box-shadow ring sits, and never blocks a click.

01/03A ring that grows in
tsx
"before:opacity-0 before:scale-[0.99]before:transition-[opacity,scale]before:duration-(--godui-duration-fast) before:ease-out-expo","has-[[data-slot=input-group-control]:focus-visible]:border-ringhas-[[data-slot=input-group-control]:focus-visible]:before:opacity-100has-[[data-slot=input-group-control]:focus-visible]:before:scale-100"
01

A ring that grows in

When the field inside the group gets focus, the ring fades in and grows from 99% to full size in 150ms on ease-out-expo, so most of the change lands in the first few frames. A uniform scale moves the long sides more than the short ones: on a 640px field a 98% start tucked the ring 3px inside the side borders on its first visible frame, while the top and bottom sat in place. 99% halves that and still reads as growing on a default-width field. The border colour snaps in the same frame, which keeps the focused state readable before the ring has finished.

The scene runs at 800ms. At the top is a plain Input, whose ring snaps because an <input> can't hold a ::before. Below it is the Input Group.

02/03Box-shadow vs a pseudo-element
tsx
// shadcn: the ring is the group's own box-shadow"transition-[color,box-shadow]has-[…:focus-visible]:ring-[3px]has-[…:focus-visible]:ring-ring/50"// GodUI: the same box-shadow, on a layer"before:pointer-events-none before:absolutebefore:-inset-px before:rounded-[inherit]before:ring-[3px] before:ring-ring/50"
02

Box-shadow vs a pseudo-element

shadcn's ring is a 3px box-shadow on the group, and fading a shadow means repainting it every frame. GodUI draws the same shadow on a ::before and fades the layer instead.

The layer has to line up exactly. It is absolute, and absolute insets are measured from inside the border, so -inset-px pushes it out by the group's 1px border to cover the border box, the box shadcn's shadow starts from. rounded-[inherit] copies the group's radius, so the 3px spread curves concentrically with the border, even on a pill-shaped group. Once focused, the two rings match pixel for pixel.

pointer-events-none lets clicks pass through the layer to addon buttons. The invalid state only swaps the layer's ring colour to destructive.

03/03Result
https://
03

The result

Click a field, then Tab between them. Each ring fades in as the previous one fades out, and the pill-shaped group's ring keeps its curve.

What's animated

On the main page, turn on Slow motion in the Focus ring example to watch the ring grow on a 900ms clock.

InteractionKeyframe / mechanismPropertiesEasingDuration
Focus ring::before transitionopacity, scaleease-out-expo150ms
Border colour, invalid coloursnapnonenonenone

Blur fades it out the same way. The ring is shadcn's: 3px of ring/50, or of destructive/20 on an invalid control (destructive/40 in dark). A layer that covers the group's border box with the same radius draws it.

Why GPU-only

Only the ::before layer's opacity and scale change, which the compositor handles without repainting the shadow. The border colour snaps, because a colour transition repaints every frame.

Reduced motion

motion-reduce:before:transition-none removes the transition: the ring appears and disappears at full size in one frame.

Replacing shadcn

npx shadcn add @godui/input-group overwrites components/ui/input-group.tsx (and installs GodUI's button, input and textarea). Exports, props and data-slot attributes match shadcn/ui new-york-v4, so existing imports keep working. It also installs godui-motion for the easing and duration tokens.

The group's focus and invalid ring classes move to its ::before (before:ring-[3px] instead of ring-[3px]). If a call site set a ring colour on the group, prefix it with before:. Don't put overflow-hidden on the group: the ring sits outside its border, like shadcn's. The layer reaches that border box with before:-inset-px, which assumes shadcn's 1px border; if you give the group a thicker one, match it (border-2 with before:-inset-0.5), or the ring starts inside the border.