Buttoncomponent

Button({ className, ...props }: ButtonProps): ReactElement
ParamType
propsButtonProps
Component props for a <Button> component. required
Return
ReactElement

Render either a <button> or an <a href=""> styled as a button, based on whether an onClick or href prop is provided.

  • Content-width by default (never grows); it won't shrink below its label. Pass full to fill the available width.
  • Light by default (a pale fill with dark text). Use solid for the main action, or plain / outline to de-emphasise.
  • color= / status= set the colour of every look.
  • Pass selected to make a group of buttons (such as tabs): the selected one keeps its look, the others drop their fill.
  • Accepts all ButtonVariants styling props plus the ClickableProps (onClick, href, disabled, etc.).

A clickable styled as a button. Renders an <a href=""> when given href, or a <button> when given onClick — the shared <Clickable> primitive picks the element, so a button is always the right semantics for what it does.

Things to know:

  • Content-width by default: it sizes to its label and never grows. Pass full to fill the available width (it then shrinks to share a row, down to the content floor).
  • There are four looks:
    • Default — a pale fill of the tint colour with the tint colour as text. Use it for most actions.
      • solid — a strong fill of the tint colour with white text. Use it for the main action on a screen, such as a form's submit button.
        • plain — no fill or border until hover or focus. Use it for chrome-level actions, such as breadcrumbs and a dialog's close button.
          • outline — like plain, but with a border until hover or focus.
  • On hover, plain and outline take the same fill as a hovered default button.
  • color= / status= move the tint anchor, so they set the colour of every look. A colourless button stays a neutral grey.
  • selected makes a group of buttons, such as tabs. selected={true} sets aria-pressed (or aria-current on a link) and keeps the button's normal look. selected={false} also drops the fill until hover or focus, like plain, so the selected button stands out. Leave it undefined for a button that is not in a group.
  • small tightens the padding.
  • It takes the block variants, like any block: space sets its outer margin (space="none" removes it), padding and indent set its inner padding, and width sizes it. A padding variant sets the padding but not the minimum height, which --button-padding and --button-height set.
  • getButtonClass(variants) returns the same className the component composes — use it to style a non-<button> element as a button when Button itself doesn't fit.
  • className attaches an app class to one button, merged after the computed classes so an app stylesheet wins — see ClassProps.

Usage

tsx
import { Button } from "shelving/ui";

<Button onClick={save} solid color="primary">Save</Button>
<Button href="/about">About</Button>
<Button onClick={remove} status="error">Delete</Button>
<Button onClick={share} outline>Share</Button>

Spacing and size

tsx
import { Button } from "shelving/ui";

// No outer margin: it sits flush with the content around it.
<Button full space="none" onClick={start}>Start</Button>

A row of buttons

tsx
import { Button } from "shelving/ui";
import { Row } from "shelving/ui";

<Row gap="small" right>
  <Button plain onClick={cancel}>Cancel</Button>
  <Button solid color="primary" onClick={submit}>Continue</Button>
</Row>

Tabs

tsx
import { Button, Row } from "shelving/ui";

<Row gap="xsmall">
  {SECTIONS.map(({ key, label }) => (
    <Button key={key} small solid selected={key === section} onClick={() => setSection(key)}>
      {label}
    </Button>
  ))}
</Row>

A one-off treatment

tsx
import { Button } from "shelving/ui";

// `.spongy-press` lives in the app's own stylesheet — use it for what the theme hooks below can't express.
<Button className="spongy-press" onClick={claim}>Claim</Button>

Reusing the button class

tsx
import { getButtonClass } from "shelving/ui";

// Style an arbitrary element as a button.
<label className={getButtonClass({ color: "primary", solid: true, small: true })}>
  Upload<input type="file" hidden />
</label>

Styling

Button paints from the tint ladder. Override these hooks at :root or any ancestor scope; apply color= / status= (on the button or an ancestor scope) to recolour the whole button, or use a per-property hook for one change.

--button-padding sets the top and bottom padding, and --button-indent the left and right. Each takes one length, not a shorthand.

Every button is at least as tall as a button with an icon, so buttons line up whether they have an icon or not, and at every text size. The minimum height is --button-icon-size plus two --button-padding plus two --button-stroke. It reads those hooks, so it stays correct when a theme changes them. Set --button-height to replace it. The small variant has its own minimum, --button-small-height. Inputs use the same formula (--input-height), so an input and a button sit at the same height by default.

The --button-* colour hooks without a look in their name paint the default look. solid has its own --button-solid-* colour hooks.

--button-shadow, --button-hover-transform and the --button-active-* pressed-state hooks are static and apply to every button, with one exception: plain and outline never paint a box shadow in any state — they have no fill until hover, so a raised edge under them reads broken. The hover and pressed transforms still apply to them, so all buttons move together. --button-transition already covers animating the press and release.

plain, outline and selected={false} share the --button-plain-* hooks: --button-plain-text recolours the label, --button-plain-hover-background / --button-plain-hover-border paint the hover and focus state, and the --button-plain-active-* pair paints the pressed state. The hover fill falls back to --button-hover-background, so plain and outline buttons always hover like a default button. --button-plain-border sets the resting border of plain (transparent by default), and --button-outline-border sets the resting border of outline.

Backgrounds paint to the button's true edge: background-origin is set to border-box, so a gradient or image background in any state reaches through the transparent border instead of stopping 2px short at the padding box.

VariableStylesDefault
--button-backgroundSurface fillvar(--tint-90)
--button-hover-backgroundSurface fill on hover / focusvar(--tint-85)
--button-hover-borderBorder on hover / focusvar(--button-stroke) solid transparent
--button-hover-transformTransform on hover / focusnone
--button-textLabel colourvar(--tint-50)
--button-borderBorder shorthandvar(--button-stroke) solid transparent
--button-strokeBorder / outline thicknessvar(--stroke-normal) (2px)
--button-radiusCorner radiusvar(--radius-xsmall) (8px)
--button-paddingTop and bottom padding (one length)var(--space-small) (12px)
--button-indentLeft and right padding (one length)var(--space-small) (12px)
--button-small-paddingTop and bottom padding when small (one length)var(--space-xxsmall) (4px)
--button-small-indentLeft and right padding when small (one length)var(--space-xxsmall) (4px)
--button-icon-sizeIcon size, and the base of the minimum heightvar(--size-icon) (24px)
--button-heightMinimum height--button-icon-size + 2 × --button-padding + 2 × --button-stroke (52px)
--button-small-heightMinimum height when small--button-icon-size + 2 × --button-small-padding + 2 × --button-stroke (36px)
--button-gapGap between icon and labelvar(--space-small) (12px)
--button-small-gapGap between icon and label when smallvar(--space-xxsmall) (4px)
--button-spaceOuter block marginvar(--space-small) (12px)
--button-fontFont familyvar(--font-body)
--button-weightFont weightvar(--weight-normal) (400)
--button-sizeFont sizevar(--size-normal)
--button-leadingLine heightvar(--leading)
--button-shadowBox shadow (never on plain)none
--button-active-backgroundSurface fill while pressedvar(--button-hover-background)
--button-active-borderBorder while pressedvar(--button-hover-border)
--button-active-shadowBox shadow while pressedvar(--button-shadow)
--button-active-transformTransform while pressedvar(--button-hover-transform)
--button-transitionTransitionall var(--duration-fast) (150ms)
--button-focus-borderFocus outlinevar(--stroke-focus) solid var(--color-focus)
--button-disabled-opacityOpacity when disabled0.5
--button-solid-backgroundSurface fill when solidvar(--tint-50)
--button-solid-textLabel colour when solidvar(--tint-100) (white)
--button-solid-hover-backgroundSurface fill on hover / focus when solidvar(--tint-55)
--button-solid-active-backgroundSurface fill while pressed when solidvar(--button-solid-hover-background)
--button-plain-textLabel colour when plain or outlinevar(--tint-50)
--button-plain-borderResting border when plainvar(--button-stroke) solid transparent
--button-outline-borderResting border when outlinevar(--button-stroke) solid var(--tint-80)
--button-unselected-backgroundResting fill when selected={false}transparent
--button-plain-hover-backgroundFill on hover / focus when plain or outlinevar(--button-hover-background)
--button-plain-hover-borderBorder on hover / focus when plain or outlinevar(--button-hover-border) (transparent)
--button-plain-active-backgroundFill while pressed when plain or outlinevar(--button-plain-hover-background)
--button-plain-active-borderBorder while pressed when plain or outlinevar(--button-plain-hover-border)

Global tokens it reads: the tint ladder --tint-50 / --tint-55 / --tint-80 / --tint-85 / --tint-90 / --tint-100, plus --size-icon, --space-small, --space-xxsmall, --radius-xsmall, --stroke-normal, --stroke-focus, --color-focus, --font-body, --weight-normal, --size-normal, --leading, and --duration-fast.

css
/* Theme: pill-shaped buttons, with roomier inline padding. */
:root {
  --button-radius: 999px;
  --button-indent: var(--space-normal);
}
css
/* Theme: outline buttons use the label colour for their edge. */
:root {
  --button-outline-border: var(--stroke-normal) solid var(--tint-50);
}
css
/* Theme: buttons are raised and press down flat — `plain` and `outline` press down too but never casts a shadow. */
:root {
  --button-shadow: 0 0.25rem 0 var(--tint-30);
  --button-active-transform: translateY(0.2rem);
  --button-active-shadow: 0 0.05rem 0 var(--tint-30);
  --button-transition: all var(--duration-fast) cubic-bezier(0.34, 1.56, 0.64, 1);
}