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.
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 chip | Mix |
|---|---|
| Fill | 32% of the hue into the background (--alpha-32) |
| Label | 48% of the hue into the ink (--alpha-48) |
| Hairline | 48% 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.
<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.
catDotStyleis a bare colored background with no text and no role. Put the name beside it and mark the dotaria-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
classNameis 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'stagvariant 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-colorsThis installs lib/category-colors.ts, which needs the kit's tokens for --cat-* and --sys-cat-*.