Skip to content

Button

Button

Triggers an action when clicked. Buttons should describe the action they perform — prefer Save changes over OK.

When to use

  • For actions in forms, dialogs, and toolbars.
  • Use <a> (or a Link component) for navigation, even if it visually looks like a button.

Anatomy

  1. Container — the <button> element. Receives the variant/tone/size data attributes that drive styling.
  2. Label — the children. May be a string, an icon, or both. Icons use the gap from --helixui-space-2.

Variants

variant controls the visual emphasis. Pair it with tone to convey intent.

<Button variant="solid">Solid</Button>
<Button variant="soft">Soft</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="outline">Outline</Button>

Tones

tone controls the color, independent of variant. The same solid button can be brand, neutral, or danger.

<Button tone="brand">Save</Button>
<Button tone="neutral">Cancel</Button>
<Button tone="danger">Delete</Button>

Sizes

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

Disabled

<Button disabled>Cannot click</Button>

Forwarding refs and props

Button is React.forwardRef<HTMLButtonElement> and forwards every standard <button> attribute. This includes onClick, aria-*, form, name, etc.

const ref = useRef<HTMLButtonElement>(null);
<Button ref={ref} onClick={handle} aria-describedby="hint" />

Do / Don’t

  • Do use one primary (solid + brand) action per view.
  • Do put destructive actions on the left and confirmation on the right in dialogs (or follow your platform convention).
  • Don’t use disabled to hide functionality — explain why the action is unavailable.
  • Don’t stack multiple solid brand buttons next to each other; demote secondary actions to soft, ghost, or outline.

Install: @helixui/core

import { Button } from '@helixui/core'

status: stable · since: 0.1.0

Tags: clickable, action, primary, destructive, submit, cta

Live playground

Open the full editor or source on GitHub.

Anatomy

┌────────────────────────────────┐
│ [icon?] Label [icon?] │ ← children = label, optional leading/trailing icon
└────────────────────────────────┘
↑ background per (variant, tone)
↑ border per (variant, tone)
↑ rounded by radius.md

Layout

  • displayinline-block
  • widthcontent
  • heightcontent
  • intrinsicSize~28px (sm) / 36px (md) / 44px (lg) tall, hugs label width
  • stackabletrue
  • fullBleedfalse

Visual

A pill-shaped, slightly rounded rectangle (radius.md). solid fills with the tone color and uses on-tone text; soft is a tinted fill of the tone with the tone’s text color; outline is transparent with a 1px tone border; ghost is fully transparent until hover. Hover and active states deepen the tone by one step. The focus ring is a 2px offset outline on :focus-visible only. Disabled buttons are 50% opacity and lose pointer events.

Props

NameTypeDefaultDescription
variant'solid' | 'soft' | 'ghost' | 'outline'solidVisual style. solid is the highest-emphasis option; ghost the lowest.
tone'brand' | 'neutral' | 'danger'brandColor intent. brand for primary actions, neutral for secondary, danger for destructive.
size'sm' | 'md' | 'lg'mdButton size. Use sm in compact UIs (toolbars), lg for prominent calls to action.
disabledbooleanfalseDisables interaction. Removes the element from the tab order.
type'button' | 'submit' | 'reset'buttonNative HTML button type. Defaults to button so the component does not accidentally submit forms.
...restButtonHTMLAttributes<HTMLButtonElement>All standard button attributes are forwarded to the underlying <button>.

Tokens used

color.bg.action.brand.default, color.bg.action.brand.hover, color.bg.action.brand.active, color.bg.action.brand.subtle, color.bg.action.neutral.default, color.bg.action.neutral.hover, color.bg.action.neutral.active, color.bg.action.danger.default, color.bg.action.danger.hover, color.bg.action.danger.active, color.bg.action.danger.subtle, color.bg.surface.default, color.text.on.brand, color.text.on.danger, color.text.primary, color.text.action.brand, color.text.action.danger, color.border.default, color.border.strong, color.border.focus, color.border.danger, radius.md, space.2, space.3, space.4, space.5, font.size.sm, font.size.md, font.size.lg, font.family.sans, font.weight.semibold, font.lineHeight.tight

Accessibility

Role: button

Keyboard

KeyAction
SpaceActivates the button.
EnterActivates the button.

Notes

  • Always provide an accessible name via children or aria-label. Icon-only buttons must use aria-label.
  • Prefer aria-disabled="true" over disabled when the action is conditionally unavailable but should remain focusable so users can discover it.
  • The focus ring is rendered with outline and only on :focus-visible, so mouse users do not see it.

Composes with

ComponentRelationNote
IconButtonalternativeUse IconButton when there is no text label.
Formparenttype="submit" submits the surrounding Form.
DialogTriggerchildCommon opener for dialogs.
StackparentGroup multiple buttons in a Stack with direction="row".
TooltipwrapsWrap with Tooltip when the action needs explanation.

Prompt examples

These are the AI prompt → JSX mappings used by the helixui prompt DSL and integrations like Cursor / Claude Code.

primary call-to-action

“add a Save button”

<Button>Save</Button>

destructive action

“make a Delete button”

<Button tone="danger">Delete</Button>

secondary action next to a primary

“Cancel + Save buttons in a row”

<Stack direction="row" gap={2} justify="end">
<Button variant="ghost" tone="neutral">Cancel</Button>
<Button>Save</Button>
</Stack>

icon-only action in a toolbar

“small ghost button with just a search icon”

<Button variant="ghost" size="sm" aria-label="Search">
<SearchIcon />
</Button>

form submission

“submit button at the bottom of a form”

<Button type="submit" size="lg">Create account</Button>