Button styles and colours

Redraw every Button in a Theme scope by its style and colour with buttonStyles and buttonColors.

Usage

import { Theme } from '@epilot/spark-ui/theme'
<Theme
  buttonStyles={{ soft: 'outline', surface: { fill: 'none', border: 'thick' } }}
  buttonColors={{ neutral: 'primary', primary: { light: '#0E7A3D' } }}
>
  <App />
</Theme>

Buttons keep styleVariant and color in code. The Theme decides how each Button style (solid, soft, surface, outline, ghost) and each Button colour (primary, neutral, danger) is drawn. A Theme without buttonStyles and buttonColors draws every button exactly as before.

Remap and Restyle

Each entry is keyed by the value in code and is either:

  • a Remap: another style or colour name. Every button with that value is drawn as the other one.
  • a Restyle: how that value looks. A style takes fill and border levels; a colour takes a design colour.

A button is resolved in two steps: first the Remap of its own value (what it is drawn as), then the Restyle of the drawn value (how that looks). Remaps take one hop only, so { soft: 'outline', outline: 'ghost' } draws a soft button as an outline.

destructive reads as danger and accent as primary. A button with neither styleVariant nor color counts as the scope's styleVariant (or solid) and primary; a button with only a color counts as solid.

buttonStyles

type SparkButtonStyles = {
  [S in SparkStyle]?: Exclude<SparkStyle, S> | SparkButtonStyleLevels
}

interface SparkButtonStyleLevels {
  fill?: 'strong' | 'subtle' | 'none'
  border?: 'none' | 'thin' | 'thick'
}

Every named style is a pair of levels, so a Restyle with a style's own levels draws exactly that style. A level left out keeps the style's own.

Stylefillborder
solidstrongnone
softsubtlenone
surfacesubtlethin
outlinenonethin
ghostnonenone

thick is a 2px border. Hover, text contrast, high contrast and dark mode follow from the levels and the button's colour.

The Theme writes the levels as --spark-button-<style>-fill and --spark-button-<style>-border on its element (0 none, 1 subtle or thin, 2 strong or thick).

buttonColors

interface SparkButtonColors {
  primary?: 'neutral' | 'danger' | SparkButtonDesignColor
  neutral?: 'primary' | 'danger' | SparkButtonDesignColor
  danger?: SparkButtonDesignColor
}

interface SparkButtonDesignColor {
  light: string
  dark?: string
}

A design colour is a hex. Spark generates a full scale from it, the same way as accentColor, with text on a solid fill kept at AA contrast. Without dark, the dark scale is generated from light.

danger can take a design colour but cannot be remapped to another colour.

Nested Themes

A nested Theme overrides single entries and inherits the rest. Dialogs and popovers opened from a scope use its entries.

<Theme buttonStyles={{ soft: 'outline', ghost: 'surface' }}>
  <Theme buttonStyles={{ soft: 'solid' }}>
    {/* soft draws solid, ghost still draws surface */}
  </Theme>
</Theme>

Skipping the Remap

<Button fixedLook> keeps its own styleVariant and color whatever the Remaps say, but still takes the Restyle of those values. Use it for a slot whose look is set on purpose, such as a portal's call to action.

<Button styleVariant="soft" color="neutral" fixedLook>
  Fixed
</Button>

styleVariant="media" ignores both maps.

On this page