FairGarden Design

Button

A Base UI Button in three builds: the solid action, its outline twin and the text button.

import { Button } from '@fairgarden-private/design/components/Button'

Labels are a verb and an object, authored in title case. Handle presses with onClick; onPress is a deprecated alias kept for existing callers. A navigating action renders an anchor through render (with nativeButton={false}), keeping link semantics.

Variants and sizes

Variants and sizes

outline is the default; solid is the page's primary action; text carries a trailing glyph. Sizes run sm (32 px, hit area extended to 44), md (40 px, default), lg (48 px) and xl (56 px, the page's single transactional action). icon adds one functional glyph; iconOnly hides the label, which stays the accessible name. A disabled button draws a dotted edge.

Clicked 0 times

ButtonVariants.tsx
'use client'

import * as React from 'react'
import { Button } from '@fairgarden-private/design/components/Button'
import styles from './variants.module.css'

export function ButtonVariants() {
  const [clicks, setClicks] = React.useState(0)
  const count = () => setClicks((value) => value + 1)

  return (
    <div className={styles.stack}>
      <div className={styles.row}>
        <Button variant="solid" onClick={count}>
          Donate Now
        </Button>
        <Button variant="outline" onClick={count}>
          Learn More
        </Button>
        <Button variant="text" icon="chevron_right" iconPosition="end" onClick={count}>
          See All Trails
        </Button>
      </div>
      <div className={styles.row}>
        <Button size="sm">Small</Button>
        <Button size="md">Medium</Button>
        <Button size="lg">Large</Button>
        <Button size="xl" variant="solid">
          Extra Large
        </Button>
      </div>
      <div className={styles.row}>
        <Button icon="download" onClick={count}>
          Download Map
        </Button>
        <Button iconOnly icon="search" onClick={count}>
          Search
        </Button>
        <Button disabled>Sold Out</Button>
      </div>
      <p className={styles.status} aria-live="polite">
        Clicked {clicks} {clicks === 1 ? 'time' : 'times'}
      </p>
    </div>
  )
}

Primary and secondary

Primary and secondary

The solid fill is the scope's action scale until secondary overrides it. Outline edges, labels and the focus ring are primary roles, so primary recolors them.

ButtonColor.tsx
import { Button } from '@fairgarden-private/design/components/Button'
import styles from './color.module.css'

/**
 * A solid Button fills with the scope's action scale unless `secondary` is
 * passed; outline edges and labels are primary roles.
 */
export function ButtonColor() {
  return (
    <div className={styles.row}>
      <Button variant="solid">Scope Action</Button>
      <Button variant="solid" secondary="indigo">
        Secondary Indigo
      </Button>
      <Button variant="outline" primary="plum">
        Primary Plum
      </Button>
      <Button variant="text" secondary="orange" icon="arrow_forward" iconPosition="end">
        Secondary Orange
      </Button>
    </div>
  )
}

On grounds

On paper, forest and leaf

The same two buttons on three grounds. On forest the action stays amber; on leaf, a solid ground with one ink, it becomes the ink pill.

paper
forest
leaf
ButtonGrounds.tsx
import { Button } from '@fairgarden-private/design/components/Button'
import { PresetGround } from '@/components/PresetGround'
import styles from './grounds.module.css'

const presets = ['paper', 'forest', 'leaf'] as const

/** Same props, three grounds: the action becomes the ink pill on leaf. */
export function ButtonGrounds() {
  return (
    <div className={styles.row}>
      {presets.map((preset) => (
        <PresetGround key={preset} preset={preset} className={styles.face}>
          <span className={styles.name}>{preset}</span>
          <div className={styles.actions}>
            <Button variant="solid">Join Us</Button>
            <Button variant="outline">Visit</Button>
          </div>
        </PresetGround>
      ))}
    </div>
  )
}

API Reference

A Base UI Button. A navigating action renders an anchor through render (with nativeButton={false}), keeping link semantics. States come from Base UI and ARIA attributes only: pass disabled (with focusableWhenDisabled where the reason matters) or aria-busy together with an authored “-ing…” label; a busy button holds its rest width and ignores clicks.

PropTypeDescription
butted
'start' | 'end' | null | undefined

Butts the button against an adjacent field on its start or end edge, as in the butted submit. Default: none.

icon
| 'menu'
| 'search'
| 'circle'
| 'arrow_forward'
| 'arrow_upward'
| 'expand_more'
| 'close'
| 'remove'
| 'add'
| 'check'
| 'chevron_right'
| 'chevron_left'
| 'more_horiz'
| 'play_arrow'
| 'pause'
| 'download'
| 'zoom_in'
| 'zoom_out'
| 'recenter'
| 'help'
| 'mail'
| undefined

One optional functional glyph (§6.10), inline tier, FILL 0.

iconOnly
boolean | null | undefined

true hides the label and shows only icon. Default false.

iconPosition
'start' | 'end' | undefined

Which side of the label the glyph sits on (a text Button’s chevron trails).

onMedia
false | boolean | null | undefined

Only with iconOnly.

onPress
PressCallback | undefined

Called on click, after onClick.

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale, from the primary roster: the outline edge, labels and focus ring. Never defaulted; omitted, it inherits the scope [D133].

secondary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale: the solid fill and the text glyph. Omitted, solid falls back to the scope’s action scale.

size
'sm' | 'md' | 'lg' | 'xl' | null | undefined

Fixed height: sm 32 px (hit area extended to 44), md 40 px (default), lg 48 px, xl 56 px for the page’s single transactional action.

variant
'text' | 'solid' | 'outline' | null | undefined

outline (default) is the outline twin; solid is the page’s primary action, filled with the scope’s action scale; text is a text button whose glyph trails (§9.2).

children
React.ReactNode | undefined

The label: verb plus object, authored in title case [D160].

button

Button classes (§9.2, §1.11.10). primary and secondary are never defaulted [D133]: the solid class falls back to the scope’s action scale for secondary, and an explicit secondary wins from the scales layer.

type button = button
ButtonProps

Props for Button: Base UI Button props plus the variant and color axes. With iconOnly, icon and an accessible-name children are required.

type ButtonProps = (
  | {
      iconOnly?: false | null;
      onMedia?: false | null;
      size?: 'sm' | 'md' | 'lg' | 'xl' | null;
      icon?:
        | 'menu'
        | 'search'
        | 'circle'
        | 'arrow_forward'
        | 'arrow_upward'
        | 'expand_more'
        | 'close'
        | 'remove'
        | 'add'
        | 'check'
        | 'chevron_right'
        | 'chevron_left'
        | 'more_horiz'
        | 'play_arrow'
        | 'pause'
        | 'download'
        | 'zoom_in'
        | 'zoom_out'
        | 'recenter'
        | 'help'
        | 'mail';
      children?: React.ReactNode;
    }
  | {
      iconOnly: true;
      onMedia?: boolean | null;
      size?: 'sm' | 'md' | 'lg' | null;
      icon:
        | 'menu'
        | 'search'
        | 'circle'
        | 'arrow_forward'
        | 'arrow_upward'
        | 'expand_more'
        | 'close'
        | 'remove'
        | 'add'
        | 'check'
        | 'chevron_right'
        | 'chevron_left'
        | 'more_horiz'
        | 'play_arrow'
        | 'pause'
        | 'download'
        | 'zoom_in'
        | 'zoom_out'
        | 'recenter'
        | 'help'
        | 'mail';
      children: React.ReactNode;
    }
) & {
  /**
   * `outline` (default) is the outline twin; `solid` is the page's primary
   * action, filled with the scope's action scale; `text` is a text button
   * whose glyph trails (§9.2).
   */
  variant?: 'text' | 'solid' | 'outline' | null;
  /**
   * Butts the button against an adjacent field on its `start` or `end`
   * edge, as in the butted submit. Default: none.
   */
  butted?: 'start' | 'end' | null;
  /**
   * Primary Radix scale, from the primary roster: the outline edge, labels
   * and focus ring. Never defaulted; omitted, it inherits the scope [D133].
   */
  primary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  /**
   * Secondary Radix scale: the `solid` fill and the `text` glyph. Omitted,
   * `solid` falls back to the scope's action scale.
   */
  secondary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | 'amber'
    | 'blue'
    | 'cyan'
    | 'grass'
    | 'green'
    | 'jade'
    | 'lime'
    | 'mint'
    | 'orange'
    | 'sky'
    | 'teal'
    | 'yellow'
    | null;
  /** Which side of the label the glyph sits on (a `text` Button's chevron trails). */
  iconPosition?: 'start' | 'end';
  /**
   * Called on click, after `onClick`.
   * @deprecated Use `onClick`. Kept as an alias from the react-aria Button.
   */
  onPress?: PressCallback;
}

Specification: DESIGN-SYSTEM.md §9.2 (button) and §1.11.10 (the reference Button).