Button
Trigger an event or action. The button family includes three components for different semantic needs.
The button family is three components for three semantic needs. Pick the right one before you reach for variants — the wrong shell semantically can't be fixed with styling.
Button
Use when: The action stays on this page (submit, open dialog, confirm).
Don't: Use it for navigation — right-click and keyboard nav break.
<Button>Save</Button>LinkButton
Use when: The action navigates to a URL. Renders an `<a>` element.
Don't: Wrap a Button in a link — that breaks accessibility.
<LinkButton href="/docs">…</LinkButton>IconButton
Use when: There's no visible text label. Single-icon trigger.
Don't: Downgrade emphasis when converting from Button — keep the variant.
<IconButton icon={…} label="Close" />Use it for
- Submitting a form
- Confirming or cancelling a destructive operation
- Opening a sheet, dialog, or popover
- Triggering an immediate, synchronous action
Import
import { Button, LinkButton, IconButton } from '@brikdesigns/bds';Anatomy
- 1Container
The hit target. Sets size, padding, focus ring, and base variant styles.
- 2Leading icon
Optional icon before the label. Use 1em sizing to scale with text.
- 3Labelrequired
The accessible name of the button. Required for Button and LinkButton.
- 4Trailing icon
Optional icon after the label. Common for "Continue →" or external-link affordances.
Variants
Twelve variants. Pick by action emphasis and destructive intent, not by aesthetics.
Standard ladder
primaryMain call-to-action. One per section.
outlineSecondary emphasis. The "alongside primary" choice.
secondaryTertiary, subtle. Supporting actions that don't compete.
ghostMinimal emphasis. Repeated actions, navigation, dismiss.
Destructive
negative is the destructive action — confirm dialogs, delete-and-go flows.
The danger / danger-outline / danger-ghost trio that used to sit below it was
retired in #1957: it carried the brand accent-red rather than the system negative, and
had zero call sites across every consumer repo. The retired spellings still resolve to
negative for one minor and warn once in the console.
Lower-emphasis destructive (an outline or ghost button that is also negative) is not
expressible today, because variant carries form and valence on one axis. Tracked in
#1983.
State variants
positive confirms a completed action. inverse and on-color are for placement on dark or branded surfaces. To mark the active option in a group, pass the boolean selected prop on top of any variant — it is a state modifier, not a variant value.
Sizes
Five sizes. md is the default. Use sm only inside dense surfaces (table rows, popovers); use lg/xl for primary CTAs on landing pages.
Icons
Use iconBefore and iconAfter props. Icons should use 1em sizing to scale with the button's font size.
<Button iconBefore={<PlusIcon />}>Add item</Button>
<Button iconAfter={<ArrowRightIcon />}>Continue</Button>
<Button iconBefore={<DownloadIcon />} variant="outline">
Export CSV
</Button>Loading
The loading prop replaces button content with a spinner while preserving button width. The button is automatically disabled during loading.
<Button loading variant="primary">
Saving...
</Button>LinkButton
LinkButton is deprecated. Prefer <Button href="..."> directly — the unified Button API renders as an <a> whenever href is set. LinkButton is retained as a thin wrapper for backward compatibility and will be removed in a future major version.
An <a> element styled as a button. Use when the action navigates to a URL. The href prop is required.
// Preferred — unified Button API
<Button href="/docs" variant="outline">
Read docs
</Button>
<Button
href="https://github.com/brikdesigns/brik-bds"
target="_blank"
rel="noopener"
iconAfter={<ExternalLinkIcon />}
>
GitHub
</Button>
// Deprecated — thin wrapper, same result
<LinkButton href="/docs" variant="outline">Read docs</LinkButton>IconButton
IconButton is deprecated. Prefer <Button icon={...} label="..."> directly — the unified Button API renders icon-only whenever icon is set with no children. IconButton is retained as a thin wrapper for backward compatibility and will be removed in a future major version.
An icon-only button with a required label prop for accessibility. The label becomes the button's aria-label.
// Preferred — unified Button API
<Button icon={<CloseIcon />} label="Close dialog" variant="ghost" />
<Button icon={<TrashIcon />} label="Delete item" variant="negative" />
// Deprecated — thin wrapper, same result
<IconButton icon={<CloseIcon />} label="Close dialog" variant="ghost" />Don't downgrade emphasis when converting Button → icon-only. A primary action stays a primary action. The 2026-04 IconButton-ghost regression came from agents converting <Button variant="primary"> into <IconButton variant="ghost">. Match the emphasis level, always.
When not to use
- Don't use
ghostfor the only action on a page. Ghost is the lowest emphasis tier — it should appear alongside a higher-emphasis sibling. - Don't stack two
primarybuttons next to each other. Only one primary action per surface. - Don't use a plain Button for navigation. If clicking takes the user to a new URL, pass
hrefso Button renders as an<a>and right-click + keyboard navigation work correctly.
Accessibility
- Renders a real
<button>element. Keyboard navigation, focus ring, andEnter/Spaceactivation come from the platform. - Accessible name comes from the visible label. For IconButton,
labelbecomesaria-label. - Disabled state uses the
disabledattribute, which removes it from the tab order. For "blocked but explainable" actions, prefer keeping it focusable and showing a tooltip. - Loading state announces via
aria-busyand disables interaction without removing focus.
API
The full prop reference (with auto-extracted TypeScript types and live controls) lives in Storybook → Components/Action/Button. Summary below.
Button
| Prop | Type | Default |
|---|---|---|
variant | ButtonVariant | 'primary' |
size | ButtonSize | 'md' |
iconBefore | ReactNode | — |
iconAfter | ReactNode | — |
loading | boolean | false |
selected | boolean | false |
fullWidth | boolean | false |
disabled | boolean | false |
LinkButton
| Prop | Type | Default |
|---|---|---|
href | string (required) | — |
variant | ButtonVariant | 'primary' |
size | ButtonSize | 'md' |
iconBefore | ReactNode | — |
iconAfter | ReactNode | — |
fullWidth | boolean | false |
children | ReactNode | — |
selected | boolean | — |
IconButton
| Prop | Type | Default |
|---|---|---|
icon | ReactNode (required) | — |
label | string (required) | — |
variant | ButtonVariant | 'ghost' |
size | ButtonSize | 'md' |
loading | boolean | false |
selected | boolean | false |
Type unions
type ButtonVariant =
| 'primary' | 'outline' | 'secondary' | 'ghost'
| 'inverse' | 'on-color'
| 'danger' | 'danger-outline' | 'danger-ghost' | 'destructive'
| 'positive';
type ButtonSize = 'tiny' | 'sm' | 'md' | 'lg' | 'xl';CSS Override API
Component-scoped CSS variables exposed on .bds-button. Set these on a wrapping element or directly on the button to override without touching BDS internals.
| Variable | Default | Controls |
|---|---|---|
--button-active-translate-y | 1px | Downward shift on :active press |
--button-focus-outline-width | 2px | Focus ring stroke width |
--button-focus-outline-offset | 2px | Gap between button edge and focus ring |
/* Example: flatten press effect in a toolbar context */
.my-toolbar {
--button-active-translate-y: 0;
--button-focus-outline-width: 3px;
}Related
- Storybook playground — every variant × size × state, live controls
- Forms pattern — Save/Cancel placement, submit-state handling
- Components index — full component catalog