Kit

Button

Interactive but stateless primitive. Five variants × eleven colors, slots, loading, and asChild — with a playground to try them.

The Button is a polymorphic interactive primitive built on top of Ark UI's ark.button. Visual treatment splits into two orthogonal props:

  • variant — the shape (filled, light, outline, subtle, default)
  • color — the palette (brand, gray, red, orange, yellow, green, teal, cyan, blue, purple, pink)

Color is delivered via CSS variables on a data-color attribute, so a single class set works for every palette and dark mode gets the right shade automatically.

import { Button } from '@42/ui-react/button';

<Button variant="filled" color="brand" onClick={() => console.log('hi')}>
  Get started
</Button>

Playground

Tweak the visual props live — controls are curated in the story file. The code panel updates as you change them.

Component

Presets

Props

Code

<Component  disabled={false}  color="brand"  variant="filled"  size="sm"  isLoading={false}>  Launch</Component>

Full prop browser

If you need to inspect every prop on ButtonProps (including the spread-through HTML attributes), use the kitchen-sink view.

Variants

Five visual treatments. Each one reads off the same color slots (--c-solid, --c-soft, --c-text), so they all swap palettes uniformly.

<Button variant="filled">Filled</Button>
<Button variant="light">Light</Button>
<Button variant="outline">Outline</Button>
<Button variant="subtle">Subtle</Button>
<Button variant="default">Default</Button>

default is intentionally palette-independent — it draws on neutral surface tokens. The color prop only tints its focus ring.

Colors

Every variant (except default) accepts any color in the palette. Adding a new color is a CSS-only change — append a [data-color="…"] block to theme.css and a name to COLORS.

<Button variant="filled"  color="brand">brand</Button>
<Button variant="light"   color="blue">blue</Button>
<Button variant="outline" color="teal">teal</Button>
<Button variant="subtle"  color="gray">gray</Button>
<Button variant="filled"  color="red"><Trash2 /> Delete</Button>

For a destructive action, use variant="filled" color="red". There is no separate destructive variant — intent is conveyed through color.

Sizes

Three sizes covering the standard density needs. For an icon-only square button, reach for ActionIcon.

<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>

Slots

leftSlot and rightSlot render before and after the children. Both are wrapped in aria-hidden so they don't pollute the accessible name. Pass a bare lucide-react icon — the button's size variant scales it automatically.

import { ArrowRight, ExternalLink, Plus } from 'lucide-react';

<Button leftSlot={<ExternalLink />}>Open</Button>
<Button rightSlot={<ArrowRight />}>Continue</Button>
<Button
  variant="outline"
  color="gray"
  leftSlot={<Plus />}
  rightSlot={<kbd>⌘N</kbd>}
>
  New file
</Button>

Loading

isLoading swaps in an inline spinner, disables pointer interaction, and sets aria-busy="true".

const [pending, startTransition] = useTransition();

<Button
  isLoading={pending}
  onClick={() => startTransition(submit)}
>
  Save changes
</Button>

While loading, any user-supplied leftSlot is suppressed so the spinner can take its place without layout shift.

Disabled

Both native :disabled and Ark's data-disabled attribute are styled, so the disabled treatment lands whether the underlying element is a <button> or rendered through asChild.

<Button disabled>Disabled</Button>

Polymorphic with asChild

Render any other element while keeping the button's styling and behavior. Useful for wrapping a Next.js <Link> so you don't lose client-side navigation.

import Link from 'next/link';

<Button asChild color="brand">
  <Link href="/dashboard">Open dashboard</Link>
</Button>

Form submission

Buttons default to type="button" to prevent accidental form submission. Opt in explicitly when you want submit semantics:

<form action={createPost}>
  <Button type="submit" color="brand">
    Publish
  </Button>
</form>

API

Prop

Type

All other props are forwarded to the underlying button (or asChild element).

Accessibility

  • Focus ring is exposed only on keyboard focus (focus-visible:), so pointer interactions stay quiet. The ring tracks color.
  • aria-busy and aria-disabled are set whenever the button is loading or disabled, regardless of the visual state.
  • Slot contents are aria-hidden; the accessible name comes from the children. For icon-only triggers, use ActionIcon, which forces an explicit aria-label.

On this page