Light

Button

Displays a button or a component that looks like a button.

Spec · from metadataOpen in Figma ↗

When to use

  • Primary call-to-action in a page or section
  • Form submission triggers
  • Navigation actions styled as buttons (use asChild with a link)
  • Icon-only actions in toolbars or compact layouts

When not to use

  • In-text navigation — use a plain anchor or Link component instead
  • Toggle state — use Toggle or Switch instead
  • Menu triggers — use DropdownMenuTrigger which composes Button internally

Variants

PropValuesDefaultDescription
variantdefaultdestructiveoutlinesecondaryghostlinkdefaultVisual hierarchy: default (primary) > secondary > outline > ghost > link. Use destructive for dangerous actions.
sizedefaultxssmlgiconicon-xsicon-smicon-lgdefaultControls height and padding. Icon sizes render as square buttons for icon-only use.

Anti-patterns

Avoid<Button><TrashIcon /></Button>
Prefer<Button size="icon" variant="destructive" aria-label="Delete item"><TrashIcon /></Button>

Icon-only buttons must specify an icon size variant and aria-label for accessibility.

Avoid<Button variant="default">Save</Button>
<Button variant="default">Cancel</Button>
Prefer<Button variant="default">Save</Button>
<Button variant="outline">Cancel</Button>

Only one primary button per section. Demote secondary actions to outline or ghost.

Avoid<Button className="bg-red-500">Delete</Button>
Prefer<Button variant="destructive">Delete</Button>

Never use arbitrary Tailwind color values. Use the destructive variant instead.

Accessibility

  • Required ARIAaria-label (when icon-only)
  • Min touch target24px
  • Screen readerRenders as a native <button> element. When using asChild, ensure the composed element is focusable and has a button role.
  • ContrastPrimary variant uses bg-primary/text-primary-foreground which meets WCAG AA.

Token bindings

TokenCategoryUsage
primarycolorDefault variant background
destructivecolorDestructive variant background
secondarycolorSecondary variant background
accentcolorGhost/outline hover state
rounded-mdradiusBorder radius
shadow-xsshadowOutline variant box shadow

[!NOTE] The default variant uses bg-primary which resolves to Helix navy blue. The outline variant uses text-primary for both label and icons — this applies in all states including hover. Always use semantic variants, not hard-coded colors.

Import

import { Button } from "@timelycare/helix-ui"

Variant styling

The canonical variant/size values and defaults live in the Spec panel above. The table below documents how each variant renders visually.

VariantBackgroundText ColorBorderShadowTheme Behavior
defaultbg-primarytext-primary-foregroundnoneshadow-xsHelix navy
secondary (deprecated)bg-secondarytext-secondary-foregroundnoneshadow-xsNeutral gray
destructivebg-destructivetext-whitenoneshadow-xsRed
outlinebg-input-bgtext-primaryborder border-bordershadow-xs
ghostbg-transparenttext-primarynonenone
linkbg-transparenttext-primarynonenone

Action Hierarchy

Choose a variant based on the action's importance, not its visual style. The hierarchy defines four tiers of emphasis plus a link style:

PriorityAction LevelVariantWhen to Use
1PrimarydefaultThe single most important action in a view. Limit to one per section.
2SecondaryoutlineSupporting actions that complement the primary action.
3TertiaryghostLow-emphasis, repeated, or supplementary actions (toolbars, table rows, dismiss).
DestructivedestructiveIrreversible or dangerous operations. Always pair with a confirmation step.
Text linklinkWhen the action should feel like navigation, not a button.

[!WARNING] The secondary (tonal fill) variant is deprecated. Use outline for secondary actions instead. The variant remains available for backward compatibility.

See Decision: Button Action Hierarchy for full rationale.


Sizes

SizeHeightPaddingUse For
xsh-7 (28px)px-2Dense UIs, inline actions, tags
smh-8 (32px)px-3Compact UIs, table rows
defaulth-9 (36px)px-4 py-2Standard actions
lgh-10 (40px)px-8Hero sections, prominent CTAs
icon-xssize-7 (28×28px)centeredCompact icon-only buttons
icon-smsize-8 (32×32px)centeredSmall icon-only buttons
iconsize-9 (36×36px)centeredStandard icon-only buttons
icon-lgsize-10 (40×40px)centeredProminent icon-only buttons

Styling

Typography

  • Font: Adelle Sans Semibold
  • Size: text-sm (14px)
  • Line height: leading-5 (20px)

Border Radius

  • All sizes: rounded-md (8px)

Icon Sizing

  • Size: size-4 (16×16px)
  • Spacing: gap-2 (8px) between icon and text using flex layout

Icons

Use data-icon attributes for automatic icon spacing within buttons. Set data-icon="inline-start" for leading icons and data-icon="inline-end" for trailing icons.

import { Mail, ArrowRight } from "lucide-react"

// Left icon (using data-icon)
<Button>
  <Mail data-icon="inline-start" />
  Login with Email
</Button>

// Right icon (using data-icon)
<Button>
  Continue
  <ArrowRight data-icon="inline-end" />
</Button>

// Icon-only
<Button size="icon" aria-label="Settings">
  <Settings />
</Button>

// Small icon-only
<Button size="icon-xs" aria-label="Close">
  <X />
</Button>

States

StateText/IconBackgroundImplementation
DefaultBase colorsBase background
HoverBase colors+10% white overlayhover:bg-primary/90 (or variant equivalent)
FocusBase colorsBase backgroundring-3 ring-ring rounded-md
PressedBase colorsBase backgroundNo additional styling (per design preference)
Disabledopacity-50opacity-50disabled prop, pointer-events-none
LoadingBase colorsBase backgrounddisabled + Spinner component with data-icon attribute

Focus Ring

// Focus state (using focus-visible to prevent click focus)
focus-visible:ring-3 focus-visible:ring-ring focus-visible:outline-none

Hover Effect

Hover applies a 10% white overlay on solid backgrounds:

// Default variant hover
hover:bg-[linear-gradient(rgba(255,255,255,0.1),rgba(255,255,255,0.1)),linear-gradient(var(--primary),var(--primary))]

// Or simplified with opacity
hover:bg-primary/90

The outline variant uses an accent background on hover while keeping text-primary for label and icons:

// Outline variant hover — background shifts to accent, text stays primary
hover:bg-accent          // light mode: light blue (#E4EEFA)
dark:hover:bg-input/50   // dark mode: input-bg at 50% opacity
// text-primary is not overridden on hover

Common Patterns

Basic Usage

<Button>Click me</Button>
<Button variant="outline">Secondary</Button>
<Button variant="destructive">Delete</Button>

Loading State

import { Spinner } from "@timelycare/helix-ui"

<Button disabled>
  <Spinner data-icon="inline-start" />
  Please wait
</Button>

As Link (Next.js)

import Link from "next/link"

<Button asChild>
  <Link href="/dashboard">Dashboard</Link>
</Button>

Icon Button

<Button size="icon" variant="ghost" aria-label="More options">
  <MoreHorizontal className="size-4" />
</Button>

Button Group

Use the ButtonGroup component for grouped actions with collapsed borders:

import { ButtonGroup } from "@timelycare/helix-ui"

<ButtonGroup>
  <Button variant="outline">Cancel</Button>
  <Button>Save Changes</Button>
</ButtonGroup>

For spaced (non-collapsed) buttons, use a flex container:

<div className="flex gap-2">
  <Button variant="outline">Cancel</Button>
  <Button>Save Changes</Button>
</div>

Accessibility

  • Use aria-label for icon-only buttons
  • Add aria-busy="true" for loading state
  • Disabled buttons get aria-disabled="true" automatically
// Icon-only with accessible name
<Button size="icon" aria-label="Settings">
  <Settings className="size-4" />
</Button>

// Loading with aria-busy
<Button disabled aria-busy="true">
  <Spinner data-icon="inline-start" />
  Saving...
</Button>

Gotchas

ProblemSolution
Icon-only button has no accessible nameAdd aria-label prop
Loading state still clickableAlways use disabled with loading
Button inside Link causes hydration errorUse asChild prop instead
Focus ring visible on clickUse focus-visible: not focus:
Icons misaligned with textUse data-icon="inline-start" or data-icon="inline-end" on the icon
Hover effect shows wrong on pressedPressed state intentionally has no special styling

See Also

  • Related Components: Toggle (toggleable button), Badge (button with badge), Button Group (grouped buttons), Spinner (loading indicator)
  • Tokens: Colors — Theme-aware color tokens

Last updated: March 3, 2026