Starter Docs
UI

Button

A native button with four styles and four sizes, a stateful label, ripple, magnetic and metallic looks. Every animation is CSS.

Installation

Add the registry to components.json, once

components.json
{
  "registries": {
    "@rabtx": "http://localhost:3002/r/{name}.json"
  }
}

Add the component

npx shadcn@latest add @rabtx/button

Or, without the namespace, by URL:

npx shadcn@latest add http://localhost:3002/r/button.json

The files land in your components/ui and are styled by your own theme. Add --overwrite to replace a shadcn component with the same name.

Usage

Installed from the registry, import from @/components/ui/button; inside this monorepo, from @rabtx/ui/button.

import { Button } from "@rabtx/ui/button";

export function Actions() {
  return (
    <>
      <Button variant="secondary">Cancel</Button>
      <Button type="submit">Save</Button>
    </>
  );
}

Hover grows the button to 1.02, only where a real mouse hovers. Pressing springs it down to 0.93 and back on a spring solved into a CSS linear() easing.

Variants

primary (the default), secondary, ghost and outline.

Icons

Put HugeIcons in the children, before or after the text. They are sized for you.

States

StatefulButton moves between idle, loading, success and error. The status icon widens in while the old one folds away, the label rolls over letter by letter, and the button eases to its new width. Loading sets aria-busy, blocks clicks and stays focusable. Click to try both.

import { type ButtonState, StatefulButton } from "@rabtx/ui/button";

const [state, setState] = useState<ButtonState>("idle");

<StatefulButton state={state} onClick={save}>
  Save changes
</StatefulButton>;

Disabled

Half opacity and no pointer events. A native disabled attribute, so it also leaves the tab order.

Ripple

ripple spreads a soft disc of the text color from where you pressed. pressScale sets how far the press goes (1 turns it off).

ButtonLink is an <a> that looks like a button, for navigation.

Magnetic

MagneticButton leans toward a mouse pointer and springs back when it leaves. strength is 0 to 1 (default 0.25). Touch, reduced motion and data-motion="off" keep it still.

Metallic

MetallicButton has a polished chrome rim that drifts on a slow loop. Hover sweeps a highlight across it. paused holds the chrome still.

API

Button takes everything a native <button> takes, plus:

Prop

Type

StatefulButton adds:

Prop

Type

MagneticButton adds strength (default 0.25). MetallicButton adds paused. ButtonLink takes the Button props on an <a>.

Outside React, buttonClass() from @rabtx/ui/button/styles returns the class names:

<button class="rx-btn rx-btn-outline rx-btn-sm">Edit</button>

Theming

The Button reads only the theme tokens, so it follows your theme with nothing to configure.

PartTokens
Primary--primary, --primary-foreground
Secondary--secondary, --secondary-foreground, edge --input
Ghost and outlinetext --muted-foreground, hover --accent / --accent-foreground, outline edge --input
Metallicface --background, shadow --shadow-raised
Focus--ring
Corners--radius-button (pill), --radius-item (icon button)
Font--font-sans
Motion--spring-press, --spring-label, --spring-magnetic

To restyle one button, set a token on it or on an ancestor, or pass Tailwind classes (they win over the component styles):

<div className="[--primary:var(--success)]">
  <Button>Approve</Button>
</div>
<Button className="rounded-md">Square-ish</Button>

pressScale changes how far a press scales (default 0.93).

How the motion works

EffectCSS
Hover and pressscale transition on the press spring, hover inside @media (hover: hover) and (pointer: fine)
Ripplea span per press positioned by CSS variables, @keyframes scale 0.05 to 1 and fade over 1.6s
Status iconsalways in the DOM; data-shown transitions width, opacity, scale and blur (in on a spring, out in 160ms)
Letter rolleach letter is an inline-block with --i; @keyframes from 105% below with blur, animation-delay: calc(var(--i) * 25ms); the leaving label rolls up and out
Widththe label box transitions width to the incoming label's measured width
Magneticthe pointer sets --_btn-mx and --_btn-my; translate transitions on the magnetic spring
Metallica gradient wider than the button drifts with an 8s @keyframes loop; hover transitions a skewed highlight across

Accessibility

  • A real <button> with type="button" by default, and a 2px focus ring for the keyboard only.
  • A rolling label is hidden from screen readers letter by letter; the whole label is read instead, and changes are announced politely.
  • Loading keeps focus and full color, sets aria-busy and blocks clicks.
  • With reduced motion labels cross-fade and nothing travels; with data-motion="off" states swap at once.

On this page