Buttons
Border Trace Button
A native button whose token-colored border segment traces the control on hover or keyboard focus.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/border-trace-button.jsonPrompt
Build a React + TypeScript + Tailwind "BorderTraceButton" component with no
animation dependency.
Contract
- Export BorderTraceButton with React.forwardRef<HTMLButtonElement>.
- Extend React.ButtonHTMLAttributes<HTMLButtonElement>, add duration?: number
in seconds, and default duration to 1.6.
- Merge className with cn(), merge consumer style after the internal animation
custom property, spread every remaining native button prop, and default
type="button" while allowing the consumer to override it.
Behavior
- Keep a normal semantic border visible at all times.
- Overlay a decorative SVG rect with pathLength="1". Give it a short dash and
a long gap so its timing is independent of the button's rendered dimensions.
- Park the dash animation until the button is hovered or keyboard-focused;
then reveal it and run repeated laps. Disabled remains a native disabled
button and cannot start the interaction.
- Clamp non-finite or extremely short duration values to a safe fallback.
Rendering & styling
- Use only semantic tokens: border-border, bg-background, bg-muted,
text-foreground, stroke-primary and ring-ring. Do not use hex colors,
fixed shadows, or a fixed corner radius.
- Read the host radius through rounded-md / var(--radius-md); the SVG follows
that token rather than owning a separate visual radius.
- Mark the SVG aria-hidden and focusable=false. The button owns the accessible
name through its children and preserves native keyboard/click behavior.
- Use motion-reduce:hidden for the moving SVG layer and
motion-reduce:transition-none for cosmetic transitions. The static border
still communicates the boundary when animation is disabled.
- Ship the keyframe in a React 19 hoisted <style href precedence> tag so the
registry item needs no Tailwind config edit and duplicate instances dedupe.
Customization levers
- Tempo: change duration per call site; leave the normalized dash geometry
alone so wide and narrow buttons keep the same lap timing.
- Trace length: tune the two strokeDasharray fractions while keeping their sum
at 1. A shorter first fraction reads as a quick spark; a longer one reads as
a progressing outline.
- Emphasis: swap stroke-primary for stroke-accent or another semantic token,
and adjust hover background opacity without changing the native contract.
- Density: override padding through className, or add a documented cva size
axis when the product needs a shared small/medium/large scale.
- Trigger: keep hover and focus paired; if product behavior calls for an
always-running trace, change only animation-play-state and still preserve the
reduced-motion branch.Concepts
- Normalized path timing —
pathLength="1"turns every perimeter into the same zero-to-one coordinate system, so a compact toolbar button and a wide CTA complete a lap in the same duration. - Progressive motion — the trace is parked until intent is visible through hover or keyboard focus; idle interfaces do not spend attention on a permanently moving decoration.
- Semantic fallback — the ordinary
border-borderoutline is the durable boundary. Reduced motion removes only the traveling highlight, never the control or its affordance. - Native contract preservation — the component forwards its ref and every button attribute, so form type, disabled state, analytics handlers and accessible naming remain consumer-owned.
- Trigger parity — hover and
focus-visiblestart the same visual response, preventing a pointer-only flourish from becoming the sole indicator of emphasis.