Category identity
The six metric families, the four roles each one exposes, and the meanings a category colour is forbidden to carry.
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
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
| 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 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
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
- 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.
- 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.
- 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.
- 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-alertnames the status axis rather than a fifth category role, and no such token is emitted. - A category colour is never a chart series colour. Series colour is governed by Chart colour.
- 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)
Applying it
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.
Don’t
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.
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.
Don’t
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.
Do
Use -accent for marks and -surface for containers, and check the pair
against The contrast floor
before shipping a new category.
Don’t
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.
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| activity ink on activity surface | light | 88.5 | 9.61:1 | Pass |
| activity ink on activity surface | dark | -83.1 | 12.58:1 | Pass |
| activity line on activity surface | light | 67.6 | 4.42:1 | Pass |
| activity line on activity surface | dark | -69.0 | 9.93:1 | Pass |
| activity accent on activity surface | light | 44.1 | 2.34:1 | Below floor |
| activity accent on activity surface | dark | -53.3 | 7.26:1 | Pass |
| activity accent on the page | light | 46.1 | 2.41:1 | Below floor |
| activity accent on the page | dark | -54.6 | 8.22:1 | Pass |
| activity ink on the page | light | 90.5 | 9.90:1 | Pass |
| activity ink on the page | dark | -84.4 | 14.24:1 | Pass |
| activity line on the band | light | 66.1 | 4.32:1 | Pass |
| activity line on the band | dark | -67.4 | 8.60:1 | Pass |
| heart ink on heart surface | light | 89.1 | 10.51:1 | Pass |
| heart ink on heart surface | dark | -80.5 | 12.55:1 | Pass |
| heart line on heart surface | light | 70.4 | 5.05:1 | Pass |
| heart line on heart surface | dark | -64.5 | 9.46:1 | Pass |
| heart accent on heart surface | light | 60.3 | 3.64:1 | Pass |
| heart accent on heart surface | dark | -48.2 | 6.68:1 | Pass |
| heart accent on the page | light | 63.2 | 3.81:1 | Pass |
| heart accent on the page | dark | -49.5 | 7.27:1 | Pass |
| heart ink on the page | light | 92.0 | 10.97:1 | Pass |
| heart ink on the page | dark | -81.8 | 13.65:1 | Pass |
| heart line on the band | light | 69.9 | 5.01:1 | Pass |
| heart line on the band | dark | -62.8 | 7.88:1 | Pass |
| labs ink on labs surface | light | 88.8 | 9.78:1 | Pass |
| labs ink on labs surface | dark | -83.1 | 12.69:1 | Pass |
| labs line on labs surface | light | 67.9 | 4.51:1 | Pass |
| labs line on labs surface | dark | -68.4 | 9.90:1 | Pass |
| labs accent on labs surface | light | 59.3 | 3.49:1 | Pass |
| labs accent on labs surface | dark | -52.7 | 7.21:1 | Pass |
| labs accent on the page | light | 61.5 | 3.60:1 | Pass |
| labs accent on the page | dark | -53.9 | 8.07:1 | Pass |
| labs ink on the page | light | 90.9 | 10.09:1 | Pass |
| labs ink on the page | dark | -84.3 | 14.22:1 | Pass |
| labs line on the band | light | 66.5 | 4.42:1 | Pass |
| labs line on the band | dark | -66.7 | 8.49:1 | Pass |
| mind ink on mind surface | light | 89.9 | 10.36:1 | Pass |
| mind ink on mind surface | dark | -81.3 | 12.56:1 | Pass |
| mind line on mind surface | light | 70.7 | 4.94:1 | Pass |
| mind line on mind surface | dark | -65.4 | 9.54:1 | Pass |
| mind accent on mind surface | light | 60.0 | 3.55:1 | Pass |
| mind accent on mind surface | dark | -49.1 | 6.77:1 | Pass |
| mind accent on the page | light | 62.8 | 3.70:1 | Pass |
| mind accent on the page | dark | -50.0 | 7.42:1 | Pass |
| mind ink on the page | light | 92.7 | 10.80:1 | Pass |
| mind ink on the page | dark | -82.2 | 13.77:1 | Pass |
| mind line on the band | light | 70.0 | 4.89:1 | Pass |
| mind line on the band | dark | -63.4 | 8.00:1 | Pass |
| nutrition ink on nutrition surface | light | 89.3 | 10.06:1 | Pass |
| nutrition ink on nutrition surface | dark | -81.9 | 12.55:1 | Pass |
| nutrition line on nutrition surface | light | 69.7 | 4.78:1 | Pass |
| nutrition line on nutrition surface | dark | -66.7 | 9.66:1 | Pass |
| nutrition accent on nutrition surface | light | 39.2 | 2.11:1 | Below floor |
| nutrition accent on nutrition surface | dark | -50.2 | 6.88:1 | Pass |
| nutrition accent on the page | light | 41.8 | 2.19:1 | Below floor |
| nutrition accent on the page | dark | -51.3 | 7.63:1 | Pass |
| nutrition ink on the page | light | 91.9 | 10.44:1 | Pass |
| nutrition ink on the page | dark | -82.9 | 13.91:1 | Pass |
| nutrition line on the band | light | 68.8 | 4.71:1 | Pass |
| nutrition line on the band | dark | -64.8 | 8.19:1 | Pass |
| sleep ink on sleep surface | light | 89.6 | 10.19:1 | Pass |
| sleep ink on sleep surface | dark | -81.9 | 12.60:1 | Pass |
| sleep line on sleep surface | light | 70.0 | 4.80:1 | Pass |
| sleep line on sleep surface | dark | -66.4 | 9.65:1 | Pass |
| sleep accent on sleep surface | light | 64.6 | 4.05:1 | Pass |
| sleep accent on sleep surface | dark | -50.4 | 6.94:1 | Pass |
| sleep accent on the page | light | 67.3 | 4.22:1 | Pass |
| sleep accent on the page | dark | -51.4 | 7.67:1 | Pass |
| sleep ink on the page | light | 92.3 | 10.60:1 | Pass |
| sleep ink on the page | dark | -82.9 | 13.92:1 | Pass |
| sleep line on the band | light | 69.1 | 4.75:1 | Pass |
| sleep line on the band | dark | -64.5 | 8.16:1 | Pass |
| activity ink on the card material | light | 93.6 | 10.34:1 | Pass |
| activity ink on the card material | dark | -83.5 | 12.91:1 | Pass |
| heart ink on the card material | light | 95.1 | 11.47:1 | Pass |
| heart ink on the card material | dark | -80.9 | 12.38:1 | Pass |
| labs ink on the card material | light | 94.0 | 10.54:1 | Pass |
| labs ink on the card material | dark | -83.5 | 12.90:1 | Pass |
| mind ink on the card material | light | 95.7 | 11.28:1 | Pass |
| mind ink on the card material | dark | -81.4 | 12.49:1 | Pass |
| nutrition ink on the card material | light | 95.0 | 10.91:1 | Pass |
| nutrition ink on the card material | dark | -82.0 | 12.61:1 | Pass |
| sleep ink on the card material | light | 95.4 | 11.08:1 | Pass |
| sleep ink on the card material | dark | -82.1 | 12.62:1 | Pass |
Components that implement this
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 rather than in a
component API.
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 |
| 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 | |
| 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
Last read through against the system on 2026-09-02. Due for review every 12 months; expiry is reported by pnpm run check:freshness.
Clinical status semantics
The four status levels steady, watch, attention and urgent. Each has a fixed meaning, a fixed tone, a named threshold owner, and meanings it is never read as.
Alarm fatigue
Escalation is budgeted rather than judged screen by screen. This page sets the ceiling, names who owns it, and says what an interface loses by overspending it.