opsinjs
HealthColour, status and attention

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

CategoryCoversCommon miscategorisationBelongs to instead
HeartBlood pressure, heart rate, resting heart rate, rhythm, variability, circulationFitness performanceActivity
ActivitySteps, workouts, active minutes, movement, energy expenditureSleep-derived recovery scoresSleep
SleepDuration, timing, consistency, disturbance, sleep-derived recoveryMood on wakingMind
NutritionIntake, hydration, glucose, cholesterol, weight and body compositionDigestive symptomsA symptom surface, which is not a metric family
MindMood, stress, cognition, wellbeing check-insSleep qualitySleep
LabsTest results, panels, assays, anything reported by a laboratoryThe interpretation of a resultThe 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 tokenWhat it paintsContrast obligationProhibited use
--opsin-category-<name>-accentThe identity mark: a rule, a dot, an icon tint or a chart accentNone. 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>-lineA boundary, an icon stroke or a chart markThe lightest value in the ramp that clears the non-text contrast floor against -surfaceText, which the non-text floor does not qualify it for
--opsin-category-<name>-surfaceA low-chroma background tint for a containerClears the contrast floor with -ink on topAny blend toward a status surface on the same element
--opsin-category-<name>-inkForeground content sitting on -surfaceLegible 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

  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.
  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)

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.

PairThemeAPCA LcWCAG 2.2Floor
activity ink on activity surfacelight88.59.61:1Pass
activity ink on activity surfacedark-83.112.58:1Pass
activity line on activity surfacelight67.64.42:1Pass
activity line on activity surfacedark-69.09.93:1Pass
activity accent on activity surfacelight44.12.34:1Below floor
activity accent on activity surfacedark-53.37.26:1Pass
activity accent on the pagelight46.12.41:1Below floor
activity accent on the pagedark-54.68.22:1Pass
activity ink on the pagelight90.59.90:1Pass
activity ink on the pagedark-84.414.24:1Pass
activity line on the bandlight66.14.32:1Pass
activity line on the banddark-67.48.60:1Pass
heart ink on heart surfacelight89.110.51:1Pass
heart ink on heart surfacedark-80.512.55:1Pass
heart line on heart surfacelight70.45.05:1Pass
heart line on heart surfacedark-64.59.46:1Pass
heart accent on heart surfacelight60.33.64:1Pass
heart accent on heart surfacedark-48.26.68:1Pass
heart accent on the pagelight63.23.81:1Pass
heart accent on the pagedark-49.57.27:1Pass
heart ink on the pagelight92.010.97:1Pass
heart ink on the pagedark-81.813.65:1Pass
heart line on the bandlight69.95.01:1Pass
heart line on the banddark-62.87.88:1Pass
labs ink on labs surfacelight88.89.78:1Pass
labs ink on labs surfacedark-83.112.69:1Pass
labs line on labs surfacelight67.94.51:1Pass
labs line on labs surfacedark-68.49.90:1Pass
labs accent on labs surfacelight59.33.49:1Pass
labs accent on labs surfacedark-52.77.21:1Pass
labs accent on the pagelight61.53.60:1Pass
labs accent on the pagedark-53.98.07:1Pass
labs ink on the pagelight90.910.09:1Pass
labs ink on the pagedark-84.314.22:1Pass
labs line on the bandlight66.54.42:1Pass
labs line on the banddark-66.78.49:1Pass
mind ink on mind surfacelight89.910.36:1Pass
mind ink on mind surfacedark-81.312.56:1Pass
mind line on mind surfacelight70.74.94:1Pass
mind line on mind surfacedark-65.49.54:1Pass
mind accent on mind surfacelight60.03.55:1Pass
mind accent on mind surfacedark-49.16.77:1Pass
mind accent on the pagelight62.83.70:1Pass
mind accent on the pagedark-50.07.42:1Pass
mind ink on the pagelight92.710.80:1Pass
mind ink on the pagedark-82.213.77:1Pass
mind line on the bandlight70.04.89:1Pass
mind line on the banddark-63.48.00:1Pass
nutrition ink on nutrition surfacelight89.310.06:1Pass
nutrition ink on nutrition surfacedark-81.912.55:1Pass
nutrition line on nutrition surfacelight69.74.78:1Pass
nutrition line on nutrition surfacedark-66.79.66:1Pass
nutrition accent on nutrition surfacelight39.22.11:1Below floor
nutrition accent on nutrition surfacedark-50.26.88:1Pass
nutrition accent on the pagelight41.82.19:1Below floor
nutrition accent on the pagedark-51.37.63:1Pass
nutrition ink on the pagelight91.910.44:1Pass
nutrition ink on the pagedark-82.913.91:1Pass
nutrition line on the bandlight68.84.71:1Pass
nutrition line on the banddark-64.88.19:1Pass
sleep ink on sleep surfacelight89.610.19:1Pass
sleep ink on sleep surfacedark-81.912.60:1Pass
sleep line on sleep surfacelight70.04.80:1Pass
sleep line on sleep surfacedark-66.49.65:1Pass
sleep accent on sleep surfacelight64.64.05:1Pass
sleep accent on sleep surfacedark-50.46.94:1Pass
sleep accent on the pagelight67.34.22:1Pass
sleep accent on the pagedark-51.47.67:1Pass
sleep ink on the pagelight92.310.60:1Pass
sleep ink on the pagedark-82.913.92:1Pass
sleep line on the bandlight69.14.75:1Pass
sleep line on the banddark-64.58.16:1Pass
activity ink on the card materiallight93.610.34:1Pass
activity ink on the card materialdark-83.512.91:1Pass
heart ink on the card materiallight95.111.47:1Pass
heart ink on the card materialdark-80.912.38:1Pass
labs ink on the card materiallight94.010.54:1Pass
labs ink on the card materialdark-83.512.90:1Pass
mind ink on the card materiallight95.711.28:1Pass
mind ink on the card materialdark-81.412.49:1Pass
nutrition ink on the card materiallight95.010.91:1Pass
nutrition ink on the card materialdark-82.012.61:1Pass
sleep ink on the card materiallight95.411.08:1Pass
sleep ink on the card materialdark-82.112.62:1Pass

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 scopeWhy this page does not decide itDecided byWhere it is covered
Adding, removing or renaming a categoryThis page states the six-family vocabulary, and changing that vocabulary is a breaking change rather than a theme settingThe design system ownerCategory palettes
Which category a borderline metric belongs toBlood glucose is defensibly Nutrition and defensibly a clinical result, and opsinjs has no basis for preferring oneThe product team, as one recorded decision applied on every surface
Symptoms, medications and appointmentsThey are not metric families and take no category colour, because they are their own surfaces
The hue valuesDerivation, gamut clamping and the P3 escalation path are colour-system concerns rather than health doctrineColour
IconsA 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.

On this page