# Color

Color tokens for text, surfaces, borders, focus, and status in light and dark schemes.

## Overview

Tempo interfaces are neutral by default. Grays carry text, surfaces, and controls. Accent hues mark status, focus, and destructive actions, and nothing else.

Every color is a token from the Web variables, such as `foreground` or `gray.900`. Each token holds a light and a dark value, so you pick a role once and the scheme picks the value.

```tsx
import { style, variants } from '@tempoxyz/ds'

export const fill = true
const roles = [
  { role: 'primary', token: 'background.primary', use: 'Page background' },
  { role: 'surface', token: 'background.surface', use: 'Cards, popups, and toasts' },
  { role: 'foreground', token: 'foreground', use: 'Primary text and icons' },
  { role: 'secondary', token: 'gray.900', use: 'Descriptions and captions' },
  { role: 'tertiary', token: 'gray.700', use: 'Placeholders and chevrons' },
  { role: 'fill', token: 'grayAlpha.100', use: 'Secondary buttons and fields' },
  { role: 'hover', token: 'grayAlpha.200', use: 'Hover fills and row dividers' },
  { role: 'pressed', token: 'grayAlpha.300', use: 'Pressed fills and separators' },
  { role: 'focus', token: 'blue.700', use: 'Focus outlines' },
  { role: 'invalid', token: 'red.800', use: 'Invalid borders and Delete' },
] as const

export function Demo() {
  return (
    <ul {...styles.list()}>
      {roles.map((entry) => (
        <li key={entry.token} {...styles.row()}>
          <span {...styles.swatch({ role: entry.role })} />
          <code {...styles.token()}>{entry.token}</code>
          <span {...styles.use()}>{entry.use}</span>
        </li>
      ))}
    </ul>
  )
}

namespace styles {
  export const list = style({
    display: 'grid',
    gap: '3',
    listStyle: 'none',
    margin: '0',
    maxWidth: '32rem !custom',
    padding: '0',
  })

  export const row = style({
    alignItems: 'center',
    columnGap: '3',
    display: 'grid',
    gridTemplateColumns: '2.5rem minmax(0, 9rem) minmax(0, 1fr) !custom',
  })

  export const swatch = variants({
    base: {
      aspectRatio: '1 !custom',
      borderColor: 'grayAlpha.200',
      borderRadius: 'lg',
      borderStyle: 'solid',
      borderWidth: '1px !custom',
      boxSizing: 'border-box',
    },
    variants: {
      role: {
        fill: { backgroundColor: 'grayAlpha.100' },
        focus: { backgroundColor: 'blue.700' },
        foreground: { backgroundColor: 'foreground' },
        hover: { backgroundColor: 'grayAlpha.200' },
        invalid: { backgroundColor: 'red.800' },
        pressed: { backgroundColor: 'grayAlpha.300' },
        primary: { backgroundColor: 'background.primary' },
        secondary: { backgroundColor: 'gray.900' },
        surface: { backgroundColor: 'background.surface' },
        tertiary: { backgroundColor: 'gray.700' },
      },
    },
  })

  export const token = style({ color: 'foreground', typography: 'label.13.mono' })

  export const use = style({ color: 'gray.900', typography: 'copy.13' })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Foundations/ColorRoles.tsx)

## Roles

Start from a role, not a hex value. These tokens cover most of a product screen.

| Token                | Role                                                                        |
| -------------------- | --------------------------------------------------------------------------- |
| `background.primary` | The page behind cards, tables, and the app shell                            |
| `background.surface` | Cards, toasts, outlined fields, and sticky table headers                    |
| `foreground`         | Primary text, icons, and the fill of primary and selected controls          |
| `gray.900`           | Secondary text, such as descriptions, captions, and table cells             |
| `gray.700`           | Placeholders, chevrons, and breadcrumb separators                           |
| `grayAlpha.100`      | Resting fill of secondary buttons, filled fields, neutral tags, and `Kbd`   |
| `grayAlpha.200`      | Hover fills and table row dividers                                          |
| `grayAlpha.300`      | Pressed fills, `Separator`, and outlined field borders                      |
| `blue.700`           | Focus outlines, the caret in `OtpField`, and the today marker in `Calendar` |
| `red.800`            | Invalid borders and outlines, and the `negative` button fill                |

Values come from the Web variables in `src/web/vars.ts`, which extend Zyzz's defaults. Switch an example's scheme to see both values of a token.

`Surface` sits on `background.primary` without a shadow. The step between the two backgrounds separates a card from the page, and dark mode lifts `background.surface` above the near-black page for the same reason.

## Gray Ramp

The gray ramp keeps Zyzz's default values. Use the solid grays for text and the alpha grays for fills and borders.

| Step       | Solid `gray.*`                                     | Alpha `grayAlpha.*`                                                   |
| ---------- | -------------------------------------------------- | --------------------------------------------------------------------- |
| 100        | Hover on a pressable `Surface`, resting `Hex` copy | Resting fills of secondary controls                                   |
| 200        | Pressed `Surface`, `Hex` copy hover                | Hover fills, table row dividers, and filled field hover               |
| 300        | Pressed `Hex` copy                                 | Pressed fills, separators, and outlined field borders                 |
| 400 to 500 | None                                               | Hover on checkboxes, radios, and outlined fields, `Choicebox` borders |
| 700        | Placeholders and secondary glyphs                  | Placeholders in `Input` and `Textarea`                                |
| 900        | Secondary text                                     | None                                                                  |
| 1000       | The same value as `foreground`                     | None                                                                  |

No component uses step 600 or 800 of either ramp.

Alpha grays darken, or in dark mode lighten, whatever sits behind them. A `grayAlpha.100` fill keeps the same relative step on `background.primary`, `background.surface`, or a tinted callout, which a solid gray can't do.

## Accent Ramps

Web re-hues Zyzz's ramps to Tempo's accents and keeps Zyzz's lightness steps. Step 700 is the accent itself and holds one value in both schemes. Switch the preview's scheme to compare light and dark values.

```tsx
import { style, variants } from '@tempoxyz/ds'

export const fill = true
const steps = ['100', '200', '300', '400', '500', '600', '700', '800', '900', '1000'] as const

export function Demo() {
  const ramps = [
    ['gray', styles.gray],
    ['grayAlpha', styles.grayAlpha],
    ['blue', styles.blue],
    ['green', styles.green],
    ['amber', styles.amber],
    ['red', styles.red],
    ['purple', styles.purple],
    ['teal', styles.teal],
    ['pink', styles.pink],
  ] as const

  return (
    <div {...styles.grid()}>
      <span />
      {steps.map((step) => (
        <span key={step} {...styles.step()}>
          {step}
        </span>
      ))}
      {ramps.map(([hue, swatch]) => (
        <div key={hue} {...styles.ramp()}>
          <span {...styles.hue()}>{hue}</span>
          {steps.map((step) => (
            <span data-swatch key={step} title={`${hue}.${step}`} {...swatch({ step })} />
          ))}
        </div>
      ))}
    </div>
  )
}

namespace styles {
  export const amber = variants({
    variants: {
      step: {
        '100': { backgroundColor: 'amber.100' },
        '1000': { backgroundColor: 'amber.1000' },
        '200': { backgroundColor: 'amber.200' },
        '300': { backgroundColor: 'amber.300' },
        '400': { backgroundColor: 'amber.400' },
        '500': { backgroundColor: 'amber.500' },
        '600': { backgroundColor: 'amber.600' },
        '700': { backgroundColor: 'amber.700' },
        '800': { backgroundColor: 'amber.800' },
        '900': { backgroundColor: 'amber.900' },
      },
    },
  })

  export const blue = variants({
    variants: {
      step: {
        '100': { backgroundColor: 'blue.100' },
        '1000': { backgroundColor: 'blue.1000' },
        '200': { backgroundColor: 'blue.200' },
        '300': { backgroundColor: 'blue.300' },
        '400': { backgroundColor: 'blue.400' },
        '500': { backgroundColor: 'blue.500' },
        '600': { backgroundColor: 'blue.600' },
        '700': { backgroundColor: 'blue.700' },
        '800': { backgroundColor: 'blue.800' },
        '900': { backgroundColor: 'blue.900' },
      },
    },
  })

  export const gray = variants({
    variants: {
      step: {
        '100': { backgroundColor: 'gray.100' },
        '1000': { backgroundColor: 'gray.1000' },
        '200': { backgroundColor: 'gray.200' },
        '300': { backgroundColor: 'gray.300' },
        '400': { backgroundColor: 'gray.400' },
        '500': { backgroundColor: 'gray.500' },
        '600': { backgroundColor: 'gray.600' },
        '700': { backgroundColor: 'gray.700' },
        '800': { backgroundColor: 'gray.800' },
        '900': { backgroundColor: 'gray.900' },
      },
    },
  })

  export const grayAlpha = variants({
    variants: {
      step: {
        '100': { backgroundColor: 'grayAlpha.100' },
        '1000': { backgroundColor: 'grayAlpha.1000' },
        '200': { backgroundColor: 'grayAlpha.200' },
        '300': { backgroundColor: 'grayAlpha.300' },
        '400': { backgroundColor: 'grayAlpha.400' },
        '500': { backgroundColor: 'grayAlpha.500' },
        '600': { backgroundColor: 'grayAlpha.600' },
        '700': { backgroundColor: 'grayAlpha.700' },
        '800': { backgroundColor: 'grayAlpha.800' },
        '900': { backgroundColor: 'grayAlpha.900' },
      },
    },
  })

  export const green = variants({
    variants: {
      step: {
        '100': { backgroundColor: 'green.100' },
        '1000': { backgroundColor: 'green.1000' },
        '200': { backgroundColor: 'green.200' },
        '300': { backgroundColor: 'green.300' },
        '400': { backgroundColor: 'green.400' },
        '500': { backgroundColor: 'green.500' },
        '600': { backgroundColor: 'green.600' },
        '700': { backgroundColor: 'green.700' },
        '800': { backgroundColor: 'green.800' },
        '900': { backgroundColor: 'green.900' },
      },
    },
  })

  export const grid = style({
    alignItems: 'center',
    columnGap: '1',
    display: 'grid',
    gridTemplateColumns: '4.5rem repeat(10, minmax(0, 1fr)) !custom',
    rowGap: '1',
    width: '100% !custom',
    selectors: {
      '& [data-swatch]': {
        borderRadius: 'md',
        boxShadow: 'inset 0 0 0 1px light-dark(rgb(0 0 0 / 0.06), rgb(255 255 255 / 0.08)) !custom',
        height: '8',
      },
    },
  })

  export const hue = style({ color: 'gray.900', typography: 'label.12' })

  export const pink = variants({
    variants: {
      step: {
        '100': { backgroundColor: 'pink.100' },
        '1000': { backgroundColor: 'pink.1000' },
        '200': { backgroundColor: 'pink.200' },
        '300': { backgroundColor: 'pink.300' },
        '400': { backgroundColor: 'pink.400' },
        '500': { backgroundColor: 'pink.500' },
        '600': { backgroundColor: 'pink.600' },
        '700': { backgroundColor: 'pink.700' },
        '800': { backgroundColor: 'pink.800' },
        '900': { backgroundColor: 'pink.900' },
      },
    },
  })

  export const purple = variants({
    variants: {
      step: {
        '100': { backgroundColor: 'purple.100' },
        '1000': { backgroundColor: 'purple.1000' },
        '200': { backgroundColor: 'purple.200' },
        '300': { backgroundColor: 'purple.300' },
        '400': { backgroundColor: 'purple.400' },
        '500': { backgroundColor: 'purple.500' },
        '600': { backgroundColor: 'purple.600' },
        '700': { backgroundColor: 'purple.700' },
        '800': { backgroundColor: 'purple.800' },
        '900': { backgroundColor: 'purple.900' },
      },
    },
  })

  export const ramp = style({ display: 'contents' })

  export const red = variants({
    variants: {
      step: {
        '100': { backgroundColor: 'red.100' },
        '1000': { backgroundColor: 'red.1000' },
        '200': { backgroundColor: 'red.200' },
        '300': { backgroundColor: 'red.300' },
        '400': { backgroundColor: 'red.400' },
        '500': { backgroundColor: 'red.500' },
        '600': { backgroundColor: 'red.600' },
        '700': { backgroundColor: 'red.700' },
        '800': { backgroundColor: 'red.800' },
        '900': { backgroundColor: 'red.900' },
      },
    },
  })

  export const step = style({ color: 'gray.900', textAlign: 'center', typography: 'label.12' })

  export const teal = variants({
    variants: {
      step: {
        '100': { backgroundColor: 'teal.100' },
        '1000': { backgroundColor: 'teal.1000' },
        '200': { backgroundColor: 'teal.200' },
        '300': { backgroundColor: 'teal.300' },
        '400': { backgroundColor: 'teal.400' },
        '500': { backgroundColor: 'teal.500' },
        '600': { backgroundColor: 'teal.600' },
        '700': { backgroundColor: 'teal.700' },
        '800': { backgroundColor: 'teal.800' },
        '900': { backgroundColor: 'teal.900' },
      },
    },
  })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Foundations/ColorRamps.tsx)

| Step       | Use for                                      | Components                                                      |
| ---------- | -------------------------------------------- | --------------------------------------------------------------- |
| 100        | Tinted backgrounds                           | `Tag`, `Banner`, and `Callout` in dark mode                     |
| 200        | A brief success fill                         | Copied state of `Snippet` and `SecretValue`, success fields     |
| 300        | A stronger tint, or hover on a tint          | `Callout` in light mode, hover on success fields                |
| 400 to 600 | Borders on tinted fills and chart series     | None                                                            |
| 700        | Solid accents, focus, and status icons       | Focus outlines, `Toast` icons, success borders                  |
| 800        | Solid fills that hold white text, and errors | The `negative` button, invalid borders                          |
| 900        | Text on a tint, and status text              | `Tag`, `Banner`, `Callout`, `Field.Error`, `Table` cell intents |
| 1000       | Highest-contrast text on a tint              | None                                                            |

### Intents

Components that report status take an `intent`, and each intent maps to one ramp. Read the intent from the message, not from the color you want.

| Intent     | Ramp     | Background                         | Text and icon                         |
| ---------- | -------- | ---------------------------------- | ------------------------------------- |
| `neutral`  | Gray     | `grayAlpha.100`                    | `foreground` or `gray.900`            |
| `info`     | `blue`   | `blue.100` (`Callout`: 300 light)  | `blue.900`, `Toast` icon `blue.700`   |
| `positive` | `green`  | `green.100` (`Callout`: 300 light) | `green.900`, `Toast` icon `green.700` |
| `warning`  | `amber`  | `amber.100` (`Callout`: 300 light) | `amber.900`, `Toast` icon `amber.700` |
| `negative` | `red`    | `red.100` (`Callout`: 300 light)   | `red.900`, `Toast` icon `red.800`     |
| `accent`   | `purple` | `purple.100`                       | `purple.900`, on `Tag` only           |

`Callout` takes step 300 in light mode, since step 100 nearly matches the page background. `Toast` keeps a neutral `background.surface` and colors only its icon. `teal` and `pink` have no intent, so keep them for charts and categories.

## Light and Dark

Every token compiles to a CSS `light-dark()` value, so it follows the inherited `color-scheme`. Set the scheme once on the root element, and the whole page switches without new classes.

```css
:root {
  color-scheme: light dark;
}
```

`light dark` follows the operating system. Set `light` or `dark` to fix the scheme on the root, or on one region such as a dark promotional panel. `vars({ colorScheme: 'dark' })` returns props that apply the variables and a scheme to one element.

```tsx
import { vars } from '@tempoxyz/ds'

export function Promo() {
  return <aside {...vars({ colorScheme: 'dark' })}>Earn on idle PathUSD</aside>
}
```

If your product offers a scheme toggle, store the choice yourself. The library doesn't persist preferences.

## Guidelines

### Choose Tokens by Role

Pick the token that names the job, such as `gray.900` for a description, rather than a step that looks right in one scheme. Role tokens keep both schemes legible, since each value was chosen for its scheme.

Never write a hex value or a `light-dark()` pair into product styles. When no token fits, propose one in the Design System project instead of adding a local color.

### Keep Text at Readable Contrast

Use `foreground` for primary text and `gray.900` for secondary text. `gray.700` measures about 3.2:1 on white, below the 4.5:1 minimum for body text. Keep it for chevrons, separators, and placeholder examples, and never put an instruction in a placeholder.

**Do:** Set descriptions in `gray.900`, which stays above 7:1 on both backgrounds.

**Don't:** Set text people need to read in `gray.700`.

### Reserve Accents for Meaning

Blue means focus or information, green means success, amber means caution, and red means failure or destruction. Using an accent for decoration weakens those meanings.

Show selection with `foreground`, the way checked checkboxes, selected calendar days, and the current page in `Pagination` do. Primary buttons use `foreground` too. Keep red for errors and for `negative` buttons on actions such as Revoke key.

### Never Use Color Alone

Color alone fails readers with low color vision, monochrome displays, and forced colors. Pair every status color with text or an icon, such as a `Tag` that says Confirmed or a `Callout.Icon`.

**Do:** Name each status in text beside its color.

**Don't:** Rely on colored dots to tell Confirmed from Failed.

In charts, add labels, patterns, or a legend that names each series. Don't distinguish a TPS line from a block time line by hue alone.

### Test Both Schemes

Check every screen in light and dark before you ship. Look for alpha fills stacked on each other, custom shadows that vanish on dark surfaces, and images or logos with no dark variant.

## Contrast

Tempo targets WCAG 2.2 AA. Body text needs 4.5:1 against its background, and text at 24px or larger, or bold text at 18.66px or larger, needs 3:1. Focus outlines, borders that show state, and meaningful icons need 3:1.

| Pair                                 | Light  | Dark   | Use                     |
| ------------------------------------ | ------ | ------ | ----------------------- |
| `foreground` on `background.surface` | 17.9:1 | 16:1   | Any text                |
| `gray.900` on `background.surface`   | 8.4:1  | 7.2:1  | Any text                |
| `gray.900` on `background.primary`   | 7.7:1  | 7.6:1  | Any text                |
| `gray.700` on `background.surface`   | 3.2:1  | 5.7:1  | Glyphs, not body text   |
| Intent 900 on intent 100             | 4.8:1+ | 5.3:1+ | Tag and Banner text     |
| Intent 900 on intent 300             | 4.5:1+ | n/a    | Callout text            |
| `blue.700` on `background.primary`   | 3.9:1  | 4.5:1  | Focus outlines          |
| `red.800` on `background.surface`    | 4.7:1  | 3.9:1  | Invalid borders         |
| White on `red.800`                   | 4.7:1  | 4.7:1  | `negative` button label |

`grayAlpha.200` and `grayAlpha.300` borders measure below 2:1. They divide content, but they don't identify a control on their own, so fields keep a fill or a label as well.

## Accessibility

* Pair every status color with text or an icon. `Callout`, `Banner`, `Tag`, and `Toast` all expect text that states the outcome.
* Keep text at 4.5:1 or more. Use `gray.900` or `foreground` for anything you need to read.
* Keep focus visible. Components draw a 2px `blue.700` outline on `:focus-visible`. Don't override it with a color change alone.
* In forced colors mode, `Button`, `Input`, `Textarea`, `NumberField`, `DatePicker`, `Combobox`, and `Autocomplete` drop their resting outline and draw a solid outline in a system color on keyboard focus.
* Respect the reader's scheme. Set `color-scheme: light dark` unless your product has a reason to fix one.

## Related

* [Typography](/docs/foundations/typography) for text sizes that change contrast thresholds
* [Accessibility](/docs/foundations/accessibility) for the full WCAG 2.2 AA checklist
* [Feedback and Status](/docs/patterns/feedback-and-status) for choosing an intent
* [Tag](/docs/components/tag), [Callout](/docs/components/callout), and [Banner](/docs/components/banner) for intent colors in context
