Button

Clickable element that triggers an action.

import { Button } from 'ui-lab-components/button';
import { Button } from 'ui-lab-components/button';
 
export default function Example() {
  return (
    <Button>
      Get started
    </Button>
  );
}
 

button/copy-action

Briefly swap the icon and label after copying so the user can see it worked, then reset. The compact icon-only version beside it relies on the icon swap alone.

Loading interactive preview
<Button
  variant="secondary"
  icon={copied ? <Icon id="check" size="1em" /> : <Icon id="link" size="1em" />}
  onPress={copyLink}
>
  {copied ? 'Copied' : 'Copy deploy link'}
</Button>
 
{/* Compact: icon-only, the check confirms the copy */}
<Button
  variant="secondary"
  aria-label={copied ? 'Copied' : 'Copy deploy link'}
  icon={copied ? <Icon id="check" size="1em" /> : <Icon id="link" size="1em" />}
  onPress={copyLink}
/>

button/destructive-confirm

Confirm swaps a settings row's text and trigger for a warning and Delete/Keep pair in place, with a compact icon-only version beside it for dense rows like token lists. Both reset if the user walks away.

Loading interactive preview
<Confirm
  mode="inline"
  severity="critical"
  auto-reset-after={5000}
  onConfirm={deleteBranch}
  labels={{
    idle: { title: 'Delete branch', body: 'feature/billing-v2 and its preview deploys.' },
    confirming: { title: 'Delete feature/billing-v2?', body: 'Open pull requests will be closed.' },
    trigger: 'Delete branch',
    confirm: 'Delete',
    cancel: 'Keep',
  }}
/>
 
{/* Compact, for dense rows */}
<Confirm
  mode="inline"
  severity="critical"
  auto-reset-after={3000}
  onConfirm={revokeToken}
  labels={{
    trigger: <><Icon id="trash" size="1em" /><span className="sr-only">Revoke token</span></>,
    confirm: 'Revoke',
    cancel: <><Icon id="close" size="1em" /><span className="sr-only">Cancel</span></>,
  }}
/>

button/icon-toolbar

Toggle buttons with aria-pressed in a labelled toolbar, shown with text labels and as a compact icon-only version beside it that relies on aria-label and Tooltip.

Loading interactive preview
<div role="toolbar" aria-label="Text formatting" className="flex gap-1">
  {FORMATS.map(({ id, label, icon }) => (
    <Button
      key={id}
      variant={active.includes(id) ? 'secondary' : 'ghost'}
      aria-pressed={active.includes(id)}
      icon={icon}
      onPress={() => toggle(id)}
    >
      {label}
    </Button>
  ))}
</div>
 
{/* Compact: icon-only, labelled by aria-label and Tooltip */}
<div role="toolbar" aria-label="Text formatting" className="flex gap-1">
  {FORMATS.map(({ id, label, icon }) => (
    <Tooltip key={id} content={label} show-arrow>
      <Button
        variant={active.includes(id) ? 'secondary' : 'ghost'}
        aria-label={label}
        aria-pressed={active.includes(id)}
        icon={icon}
        onPress={() => toggle(id)}
      />
    </Tooltip>
  ))}
</div>

button/pending-action

Disable the button, set aria-busy, and show a spinner while the save runs so it can't be submitted twice. The compact icon-only version beside it swaps its icon for the spinner.

Loading interactive preview
<Button
  disabled={pending}
  aria-busy={pending}
  icon={pending ? <Icon id="spinner" size="1em" className="animate-spin" /> : undefined}
  onPress={save}
>
  {pending ? 'Saving…' : 'Save changes'}
</Button>
 
{/* Compact: the spinner replaces the icon */}
<Button
  disabled={pending}
  aria-busy={pending}
  aria-label={pending ? 'Saving' : 'Save changes'}
  icon={pending ? <Icon id="spinner" size="1em" className="animate-spin" /> : <Icon id="save" size="1em" />}
  onPress={save}
/>

button/progress-action

The button background doubles as the progress bar: a hard-stop gradient on the --background-image surface variable fills as the upload runs. Shown next to a label-only version for comparison.

Loading interactive preview
<Button
  variant="secondary"
  disabled={uploading}
  aria-busy={uploading}
  icon={<Icon id="upload" size="1em" />}
  onPress={upload}
  style={uploading ? {
    '--background-image': `linear-gradient(to right, color-mix(in oklab, currentColor 18%, transparent) ${progress}%, transparent 0)`,
  } : undefined}
>
  {uploading ? `Uploading… ${progress}%` : 'Upload files'}
</Button>

button/social-buttons

Map providers to left-aligned buttons with brand icons, using color for brands that need their own tint.

Loading interactive preview
{PROVIDERS.map(({ name, icon, color }) => (
  <Button
    key={name}
    variant="secondary"
    color={color}
    icon={icon}
    styles={{ root: 'justify-start' }}
    onPress={() => signIn(name)}
  >
    Continue with {name}
  </Button>
))}

button/split-action

Group.Button and Group.Select share one control, so the main action runs the selected merge strategy and the chevron changes it.

Loading interactive preview
<Group orientation="horizontal" spacing="none">
  <Group.Button onPress={() => merge(strategy)}>{strategy}</Group.Button>
  <Divider orientation="vertical" />
  <Group.Select selectedKey={strategy} onSelectionChange={(key) => setStrategy(String(key))}>
    <Select.Trigger aria-label="Merge strategy" styles={{ value: 'sr-only' }} />
    <Select.Content align="end">
      <Select.List>
        {STRATEGIES.map((s) => (
          <Select.Item key={s} value={s}>{s}</Select.Item>
        ))}
      </Select.List>
    </Select.Content>
  </Group.Select>
</Group>

button/toggle-group

Give Group a value and onChange and its Group.Button children act as a single-selection segmented control.

Loading interactive preview
<Group spacing="sm" aria-label="View" value={view} onChange={(value) => setView(String(value))}>
  {VIEWS.map(({ value, label }) => (
    <Group.Button key={value} value={value} variant="ghost" size="sm">
      {label}
    </Group.Button>
  ))}
</Group>