---
title: "Category identity"
description: "The six metric families, the four roles each one exposes, and the meanings a category colour is forbidden to carry."
url: "https://opsinjs.pensievelabs.org/health/category-identity"
source: "https://opsinjs.pensievelabs.org/health/category-identity.md"
section: "Health"
kind: "health"
evidence: "opinion"
reviewed: "2026-09-02"
reviewer: "design"
aliases: ["category colour", "metric category", "heart colour", "sleep colour", "metric family"]
implements: ["metric-tile", "result-card", "trend-sparkline", "score-dial", "card", "surface", "status-pill", "goal-ring"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="health" />

## What this means [#what-this-means]

The category axis marks which of the six metric families a tile, card or chart
belongs to, and recognition is the whole of its job. It is built to be read in
peripheral vision: on a dashboard carrying nine metric tiles, a reader looking
for the sleep one is meant to find it from hue and shape before reading any
label. opsinjs has not measured recognition speed, and the argument for keeping
the channel is set out under "Why (evidence)" below.

The channel depends on three properties of the palette. The six hues are low in
chroma, so a category tint can sit behind content without competing with it for
attention. A category colour is invariant across the values it accompanies. Each
hue maps to exactly one family, and no family carries a second hue anywhere in
the product.

When a category colour varies with the value it accompanies, the recognition cue
and the status cue collide on a single element. A sleep tile that turns amber
because the night was short has stopped marking the sleep family at the moment a
reader's reason to look for it is strongest.

Category colour therefore carries less semantic load than any other colour in
opsinjs: it encodes family membership and nothing else, and the meanings it may
not carry are listed under "The rule" below.

## The rule [#the-rule]

**A category colour identifies which family a metric belongs to. It may never
express value, quality, severity, direction, urgency, or brand.**

### The six categories and their boundaries [#the-six-categories-and-their-boundaries]

| Category      | Covers                                                                           | Common miscategorisation       | Belongs to instead                              |
| ------------- | -------------------------------------------------------------------------------- | ------------------------------ | ----------------------------------------------- |
| **Heart**     | Blood pressure, heart rate, resting heart rate, rhythm, variability, circulation | Fitness performance            | Activity                                        |
| **Activity**  | Steps, workouts, active minutes, movement, energy expenditure                    | Sleep-derived recovery scores  | Sleep                                           |
| **Sleep**     | Duration, timing, consistency, disturbance, sleep-derived recovery               | Mood on waking                 | Mind                                            |
| **Nutrition** | Intake, hydration, glucose, cholesterol, weight and body composition             | Digestive symptoms             | A symptom surface, which is not a metric family |
| **Mind**      | Mood, stress, cognition, wellbeing check-ins                                     | Sleep quality                  | Sleep                                           |
| **Labs**      | Test results, panels, assays, anything reported by a laboratory                  | The interpretation of a result | The status axis, which is not a category        |

There is no **Cycle** category. Menstrual and reproductive tracking is the worked
example on [Category palettes](../theming/category-palettes.mdx) for adding a
seventh ramp, and `tokens/color.json` files it under `categoryExtensionExample`
with a comment that begins "NOT SHIPPED". Nothing emits
`--opsin-category-cycle-*`.

### The four role tokens [#the-four-role-tokens]

Each category exposes four roles and no others.

| Role token                        | What it paints                                                   | Contrast obligation                                                                                                    | Prohibited use                                                                       |
| --------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `--opsin-category-<name>-accent`  | The identity mark: a rule, a dot, an icon tint or a chart accent | None. The value is chosen for recognition rather than for contrast, and several accents fail the text floor by design. | Text at any size, a boundary that is the only cue, and a large fill behind body text |
| `--opsin-category-<name>-line`    | A boundary, an icon stroke or a chart mark                       | The lightest value in the ramp that clears the non-text contrast floor against `-surface`                              | Text, which the non-text floor does not qualify it for                               |
| `--opsin-category-<name>-surface` | A low-chroma background tint for a container                     | Clears the contrast floor with `-ink` on top                                                                           | Any blend toward a status surface on the same element                                |
| `--opsin-category-<name>-ink`     | Foreground content sitting on `-surface`                         | Legible on `-surface` and on the page background, in both themes                                                       |                                                                                      |

The status axis exposes the same four role names, which is what lets a component
switch on a role without knowing which axis supplied the value.

### The six prohibitions [#the-six-prohibitions]

1. **A category colour never changes with a value.** The sleep tile is the same
   colour when sleep was four hours and when it was nine.
2. **A category colour never encodes quality.** No family is assigned a "good"
   green or a "bad" red, because the six hues are spaced for mutual
   distinguishability rather than for connotation.
3. **A category colour is never the only cue.** Every category-coloured element
   also carries the family name in text, because hue alone is not a reliable
   channel at tile scale under display and lighting conditions the product does
   not control, such as a poor display in bright sun.
4. **A category namespace never gains a role that carries a verdict.** The four
   roles are contrast roles, defined by where the colour goes and what it must be
   legible against, so a proposed `--opsin-category-heart-alert` names the status
   axis rather than a fifth category role, and no such token is emitted.
5. **A category colour is never a chart series colour.** Series colour is
   governed by
   [Chart colour](../foundations/data-visualisation/chart-colour.mdx).
6. **A category colour is never a brand accent.** A product's accent lives in the
   brand namespace and never appears as a seventh category.

## Why (evidence) [#why-evidence]

<ResearchNote evidence="opinion" date="2026-09-02">
  Every claim on this page is opinion rather than citation. We chose six
  categories because six is what a general consumer health product tends to need,
  and because six hues can be spaced far enough apart in OKLCH to stay
  distinguishable while all clearing the same contrast floor. The spacing
  constraint is measurable; the number six is a judgement about product scope.

  The restriction of category colour to wayfinding rests on the argument set out
  on [The two colour axes](./two-colour-axes.mdx): a channel that carries meaning
  on some elements and not on others has to be checked on every element before it
  can be relied on, and we design on the assumption that readers do not perform
  that check.

  The design carries one identified risk: category colour is decorative by
  construction, which makes it a standing deletion candidate whenever a team is
  cutting scope, and the argument for keeping it is recognition speed.
  Recognition speed is a longitudinal effect that accrues across repeated
  sessions with a familiar layout, and a single short usability session on a
  screen the participant has never seen will not surface it. We have not measured
  it, and the category palette is kept on the argument above rather than on a
  measurement.

  What would change our mind: evidence that consistent iconography and position
  alone deliver the same recognition speed as iconography, position and hue
  together, in which case the category palette could be retired and the four role
  tokens with it.
</ResearchNote>

## Applying it [#applying-it]

<DoDont>
  <DoDont.Do>
    Give the tile a persistent category identity, such as a `-accent` rule at the
    top edge or a tinted icon, and put the status on a separate indicator inside
    it. The two cues then resolve from different token namespaces and can be
    checked one at a time.
  </DoDont.Do>

  <DoDont.Dont>
    Fill the whole tile with `-surface` and then also tint it toward
    `--opsin-status-attention-surface` when the reading is high. The blended
    result is outside every measured contrast pair, and a single painted surface
    resolving from both namespaces is the case the two-axis rule exists to
    prevent.
  </DoDont.Dont>
</DoDont>

<DoDont>
  <DoDont.Do>
    Keep one metric in one category everywhere in the product. Weight is
    Nutrition on the dashboard, in search, in the trend screen and in the export.
  </DoDont.Do>

  <DoDont.Dont>
    Let weight be Nutrition on one screen and Activity on another because it
    fitted the layout. A metric with two category colours gives the reader a
    landmark that points at the wrong family, which means the label has to be
    read on every screen and the axis supplies nothing the label did not already
    supply.
  </DoDont.Dont>
</DoDont>

<DoDont>
  <DoDont.Do>
    Use `-accent` for marks and `-surface` for containers, and check the pair
    against [The contrast floor](../foundations/materials/the-contrast-floor.mdx)
    before shipping a new category.
  </DoDont.Do>

  <DoDont.Dont>
    Set body text in `-accent` on a category surface. Accents are selected for
    chroma and separation rather than for reading, and several of them fail the
    text floor by design.
  </DoDont.Dont>
</DoDont>

<ContrastReport scope="category" />

## Components that implement this [#components-that-implement-this]

{/* Generated from `implements`. Do not restate the list by hand. */}

Components that accept a `category` expose it as a `data-category` attribute so
the identity is available to CSS and to tests without a prop drill, and they
refuse to accept a `status` for the same painted surface. The
one-metric-one-category rule is a product decision rather than a component
concern, so no component can enforce it and it belongs on the
[Safety review checklist](./safety-review-checklist.mdx) rather than in a
component API.

## What this does not cover [#what-this-does-not-cover]

| Out of scope                                  | Why this page does not decide it                                                                                                                                   | Decided by                                                          | Where it is covered                                   |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | ----------------------------------------------------- |
| Adding, removing or renaming a category       | This page states the six-family vocabulary, and changing that vocabulary is a breaking change rather than a theme setting                                          | The design system owner                                             | [Category palettes](../theming/category-palettes.mdx) |
| Which category a borderline metric belongs to | Blood glucose is defensibly Nutrition and defensibly a clinical result, and opsinjs has no basis for preferring one                                                | The product team, as one recorded decision applied on every surface |                                                       |
| Symptoms, medications and appointments        | They are not metric families and take no category colour, because they are their own surfaces                                                                      |                                                                     |                                                       |
| The hue values                                | Derivation, gamut clamping and the P3 escalation path are colour-system concerns rather than health doctrine                                                       |                                                                     | [Colour](../foundations/colour/index.mdx)             |
| Icons                                         | A category icon is governed separately from the colour ramp, and opsinjs's view that the icon is the stronger recognition cue is opinion rather than a measurement |                                                                     |                                                       |

## Updates to this page [#updates-to-this-page]

<Reviewed />
