Skip to content
LuoForge/Tungsten
Components/Button·

Button

since v0.1.0

Primary interactive element — use for all user-initiated actions.

Variants:

  • primary (default) — ink fill, the everyday button. Use for most actions.
  • brand — orange CTA. Reserve for the one ember moment per surface. Opt-in.
  • outline — hairline border on transparent. Lower-emphasis alternative.
  • ghost — text-only. Lowest emphasis.
  • destructive — hairline + error tint. Destructive or irreversible actions.

Pass href to render as an <a> automatically, or as={Link} for Next.js Link.

For non-<button> elements, disabled and loading set aria-disabled, block pointer events, and no-op onClick.

While loading, the control sets aria-busy and adds an sr-only "Loading…" affordance so the busy state is announced (the spinner SVG itself is aria-hidden).

Install

Add the package and import the component.

pnpm add @hey-mike/tungsten
pnpm add @hey-mike/tungsten
import { Button } from '@hey-mike/tungsten';
import { Button } from '@hey-mike/tungsten';

Preview

Same fixtures used by the visual-regression suite.

Usage

apps/docs/app/snapshots/button/page.tsx

import { Button } from '@hey-mike/tungsten';
import { VariantGrid } from '../_components/VariantGrid';

export default function ButtonSnapshot() {
  return (
    <VariantGrid
      title="Button"
      variants={[
        { label: 'primary', node: <Button>Primary</Button> },
        { label: 'brand', node: <Button variant="brand">Brand</Button> },
        { label: 'outline', node: <Button variant="outline">Outline</Button> },
        { label: 'ghost', node: <Button variant="ghost">Ghost</Button> },
        { label: 'destructive', node: <Button variant="destructive">Destructive</Button> },
        { label: 'sm', node: <Button size="sm">Small</Button> },
        { label: 'md', node: <Button size="md">Medium</Button> },
        { label: 'lg', node: <Button size="lg">Large</Button> },
        { label: 'disabled', node: <Button disabled>Disabled</Button> },
        { label: 'loading', node: <Button loading>Saving…</Button> },
      ]}
    />
  );
}
import { Button } from '@hey-mike/tungsten';
import { VariantGrid } from '../_components/VariantGrid';

export default function ButtonSnapshot() {
  return (
    <VariantGrid
      title="Button"
      variants={[
        { label: 'primary', node: <Button>Primary</Button> },
        { label: 'brand', node: <Button variant="brand">Brand</Button> },
        { label: 'outline', node: <Button variant="outline">Outline</Button> },
        { label: 'ghost', node: <Button variant="ghost">Ghost</Button> },
        { label: 'destructive', node: <Button variant="destructive">Destructive</Button> },
        { label: 'sm', node: <Button size="sm">Small</Button> },
        { label: 'md', node: <Button size="md">Medium</Button> },
        { label: 'lg', node: <Button size="lg">Large</Button> },
        { label: 'disabled', node: <Button disabled>Disabled</Button> },
        { label: 'loading', node: <Button loading>Saving…</Button> },
      ]}
    />
  );
}

Props

Surface specific to <Button />.

PropTypeDefaultDescription
asElementTypeRender as a different element. Pass as={Link} for Next.js Link.
variant"primary" | "brand" | "outline" | "ghost" | "destructive"primaryVisual treatment.
size"sm" | "md" | "lg"mdControl height + padding.
loadingbooleanfalseShows a spinner and sets aria-busy. Blocks onClick while true.
iconBeforeReactNodeLeading icon rendered in a pill.
iconAfterReactNodeTrailing icon rendered in a pill.

Used in recipes

Compositions from the /recipes reference that use this component.

Source