[!NOTE] The
defaultvariant usesbg-primarywhich resolves to Helix navy blue. Theoutlinevariant usestext-primaryfor 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.
| Variant | Background | Text Color | Border | Shadow | Theme Behavior |
|---|---|---|---|---|---|
default | bg-primary | text-primary-foreground | none | shadow-xs | Helix navy |
secondary (deprecated) | bg-secondary | text-secondary-foreground | none | shadow-xs | Neutral gray |
destructive | bg-destructive | text-white | none | shadow-xs | Red |
outline | bg-input-bg | text-primary | border border-border | shadow-xs | — |
ghost | bg-transparent | text-primary | none | none | — |
link | bg-transparent | text-primary | none | none | — |
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:
| Priority | Action Level | Variant | When to Use |
|---|---|---|---|
| 1 | Primary | default | The single most important action in a view. Limit to one per section. |
| 2 | Secondary | outline | Supporting actions that complement the primary action. |
| 3 | Tertiary | ghost | Low-emphasis, repeated, or supplementary actions (toolbars, table rows, dismiss). |
| — | Destructive | destructive | Irreversible or dangerous operations. Always pair with a confirmation step. |
| — | Text link | link | When the action should feel like navigation, not a button. |
[!WARNING] The
secondary(tonal fill) variant is deprecated. Useoutlinefor secondary actions instead. The variant remains available for backward compatibility.
See Decision: Button Action Hierarchy for full rationale.
Sizes
| Size | Height | Padding | Use For |
|---|---|---|---|
xs | h-7 (28px) | px-2 | Dense UIs, inline actions, tags |
sm | h-8 (32px) | px-3 | Compact UIs, table rows |
default | h-9 (36px) | px-4 py-2 | Standard actions |
lg | h-10 (40px) | px-8 | Hero sections, prominent CTAs |
icon-xs | size-7 (28×28px) | centered | Compact icon-only buttons |
icon-sm | size-8 (32×32px) | centered | Small icon-only buttons |
icon | size-9 (36×36px) | centered | Standard icon-only buttons |
icon-lg | size-10 (40×40px) | centered | Prominent 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
| State | Text/Icon | Background | Implementation |
|---|---|---|---|
| Default | Base colors | Base background | — |
| Hover | Base colors | +10% white overlay | hover:bg-primary/90 (or variant equivalent) |
| Focus | Base colors | Base background | ring-3 ring-ring rounded-md |
| Pressed | Base colors | Base background | No additional styling (per design preference) |
| Disabled | opacity-50 | opacity-50 | disabled prop, pointer-events-none |
| Loading | Base colors | Base background | disabled + 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-labelfor 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
| Problem | Solution |
|---|---|
| Icon-only button has no accessible name | Add aria-label prop |
| Loading state still clickable | Always use disabled with loading |
| Button inside Link causes hydration error | Use asChild prop instead |
| Focus ring visible on click | Use focus-visible: not focus: |
| Icons misaligned with text | Use data-icon="inline-start" or data-icon="inline-end" on the icon |
| Hover effect shows wrong on pressed | Pressed 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