Gesso
Data display

Category colors

Nine categorical hues as code, with a readable chip style and a solid dot style that keep their meaning in every look.

Category colors give a value its own hue, like a priority that is orange at High or a status that is green at Done. They are also the way to show a standalone label such as "Beta" or "Pro", as a category chip. The CategoryColor type names the nine hues, CAT_COLORS lists them, and catChipStyle and catDotStyle return the inline styles that paint a chip and a swatch. They hold the same color in every look, which is what a role color, like Badge or Chip, does not do. A table cell wears them as CategoryTag, from Cell fields, and the markdown editor uses them for field chips, tags, and highlights.

GrayRedOrangeAmberYellowGreenBluePurplePink

Usage

import { catChipStyle, catDotStyle, type CategoryColor } from '@/lib/category-colors';

const color: CategoryColor = 'orange';

<span
  style={catChipStyle(color)}
  className="inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium"
>
  High
</span>

The style sets the fill, the ink, and the border color, and nothing else. You supply the border width, the corners, the padding, and the type, so the chip is a bordered element. For a ready chip, use CategoryTag, which is Badge's tag variant with this style on it, and use catChipStyle directly for a chip of your own, like a field in an editor.

<CategoryTag color={option.color}>{option.label}</CategoryTag>

In the projects demo, High is { label: "High", color: "orange" }. A select option in a table carries a color: CategoryColor, and the cell renders that option. The cell has no color of its own.

A chip's fill, ink, and hairline

catChipStyle(color) mixes the hue into the page's own background and ink, in OKLCH, with the chroma of those two removed so the hue is all that comes through. The fill, the label, and the border take different amounts.

Part of the chipMix
Fill32% of the hue into the background (--alpha-32)
Label48% of the hue into the ink (--alpha-48)
Hairline48% of the hue into the background (--alpha-48)

The weights are tuned in the source for AA label contrast on the fill, for every hue, in light and dark, on base, warm, and cool paper. A lighter fill needs more ink and not more hue. The mix reads --background and --foreground, so the style follows light and dark and the paper's warmth on its own.

The dot is the solid hue

catDotStyle(color) returns only a backgroundColor of the solid hue, with no mixing, for a color dot in a picker or a legend. Give the dot its own size and rounded-full, and put the name beside it.

GrayRedOrangeAmberYellowGreenBluePurplePink
<span aria-hidden style={catDotStyle(color)} className="size-3 rounded-full" />

Where the hues come from

The nine hues are gray, red, orange, amber, yellow, green, blue, purple, and pink. Each is a solid primitive, like --cat-orange, and the system adds a flat chroma to it as --sys-cat-*.

--sys-cat-orange: oklch(from var(--cat-orange) l calc(c + var(--chrome-chroma-tag)) h);

Gray has no hue of its own, so it takes the room's hue, and every other hue gains a little of the room's warmth. A look's brand retune remaps --brand-* and leaves these primitives where they are, so High stays orange whether the primary is ink, wave blue, or violet. A look may warm or cool red, orange, and yellow, as Washi does with a brick red, and High is still an orange, only Washi's.

Accessibility

  • A chip's color never carries the value alone. The label is text, so a screen reader and a reader who cannot tell the hues apart get the same information. Keep the name in the chip.
  • catDotStyle is a bare colored background with no text and no role. Put the name beside it and mark the dot aria-hidden, as above, so a screen reader reads the name once.
  • The chip's label is tuned for AA text contrast on its fill, in light and dark and on every paper. Yellow, the brightest hue, sits right at that line in light. A fill or ink you override in className is outside that tuning.
  • In forced colors, such as Windows high contrast, the browser replaces the fill, ink, and border with the system palette, so the hue is lost and only the label remains. Do not rely on hue to tell two values apart.
  • The style adds no motion and no focus behavior. The element you put it on owns both.

When to use something else

  • Badge for a read-only status or count whose color follows the look, like destructive red or the primary fill. A category hue is for a value that must stay the same color in every look.
  • Chip for a value a person picked or can dismiss, in a role color. Reach for a category color on it only when the hue belongs to the value.
  • CategoryTag, in Cell fields, when the colored value is a table cell option. It renders Badge's tag variant with this style and an optional remove button, so you do not build the chip yourself.
  • Avatar for a person. Its fallback is initials on a neutral fill, not a category hue.

Install

shadcn add @gesso/category-colors

This installs lib/category-colors.ts, which needs the kit's tokens for --cat-* and --sys-cat-*.

On this page