Skip to content
KONIGI

Tokens and constraints

Tailwind and shadcn/ui solve the primitives. What they can't say is which primitive a screen calls for, or which of the four reds on it means danger. This is the part that answers that, as tokens a build can read and rules a build can fail on.

27 extension tokens · 23 constraints · 109 implementations

What shadcn already names 19

These are overridden, not replaced. Where shadcn has a name the tokens use it, so a snippet pastes into a shadcn project unchanged.

--background
--foreground
--card
--card-foreground
--popover
--popover-foreground
--primary
--primary-foreground
--secondary
--secondary-foreground
--muted
--muted-foreground
--accent
--accent-foreground
--destructive
--destructive-foreground
--border
--input
--ring

Status 4

Severity as a closed set. shadcn ships --destructive, which is a destructive action rather than a state of the world.

--status-critical #9B1627 / #F1686D
--status-warn #9D6100 / #F0A94E
--status-nominal #28A64F / #81F597
--status-unknown #7A828F / #A4ACB9

Data state 2

Whether a number is still arriving. Shares hues with status because it is the same class of problem.

--state-stale #9D6100 / #F0A94E
--state-live #28A64F / #81F597

Series 8

Eight mutually separable categorical colours. shadcn stops at five and has no opinion about the sixth.

--chart-1 #D07526 / #C76E1E
--chart-2 #9A9000 / #928800
--chart-3 #006863 / #009A92
--chart-4 #009FBF / #00ECFF
--chart-5 #0061A1 / #0091E6
--chart-6 #7F84FB / #767CF2
--chart-7 #8B329F / #FFB6FF
--chart-8 #EA54A4 / #E14B9C

Sequential 5

Monotonic in lightness, so it survives greyscale and invents no boundary.

--scale-seq-1 #88E8FF / #004B84
--scale-seq-2 #5BC1FF / #006DA9
--scale-seq-3 #249BDB / #0190D0
--scale-seq-4 #0077B4 / #4DB6F8
--scale-seq-5 #00548E / #7BDDFF

Diverging 5

A real zero in the middle and arms that match each other in lightness.

--scale-div-neg-2 #A3514E / #B35C59
--scale-div-neg-1 #EA7D78 / #F4847F
--scale-div-mid #F2F0EC / #2A2724
--scale-div-pos-1 #00B3BA / #00BCC2
--scale-div-pos-2 #007A7F / #00888D

Direction 3

Named for direction, never sentiment. Churn rising is not good news.

--direction-up #28A64F / #81F597
--direction-down #9B1627 / #F1686D
--direction-flat #7A828F / #A4ACB9

Constraints 23

Every one derived from a checklist line in the entry it belongs to, and carrying that line. Roughly half are machine-checkable, which is worth saying plainly: claiming all of them would be the fastest way to lose the argument.

12 checked in CI 7 in the agent rules 4 human judgement

  • color.series-assigned-by-key ci

    Series colours are looked up by series key, never indexed by array position. Indexing means the same series changes colour when a filter removes a row above it.

    From categorical series palette: "Is colour assigned deterministically from the series name, or by result order?"

  • color.series-mutually-separable ci

    Every pair of --chart-* values is at least ΔE00 10 apart, so two series never read as the same colour at the width of a line.

    From categorical series palette: "Do any two adjacent-in-value series share similar hues?"

  • color.series-overflow-defined agent

    Past the palette limit the remainder collapses into one muted "other" rather than recycling hues. Two series sharing a colour is worse than one having none.

    From categorical series palette: "Is there a defined behaviour past the palette's limit?"

  • state.failure-modes-distinct agent

    Empty, stale and failed are three states with three treatments and three next actions. A stale panel keeps its last good value and says so; it never looks like a fresh one.

    From error and stale state: "Is a stale value visually distinguishable from a current one, or only from a missing one?"

  • delta.window-required agent

    A delta always names what it is measured against. The window is a required prop rather than an optional caption, because a percentage with no basis is not a fact a viewer can act on.

    From delta indicator: "Is the comparison window stated, and is it the same one across the page?"

  • delta.no-sentiment-tokens ci

    A delta uses --direction-* and a glyph, never a token named for sentiment. The metric decides whether up is good.

    From delta indicator: "Is direction encoded by anything other than hue?"

  • freshness.reports-arrival-not-refresh agent

    The indicator reports the arrival time of the newest sample against its expected cadence. A refresh that succeeds against a stalled pipeline must not read as fresh.

    From freshness indicator: "Does this show when the data arrived, or only when the page refreshed?"

  • color.alert-budget human

    Chrome, labels and resting values are neutral. Saturated colour is reserved for a state that wants a person, and anything permanently coloured has spent the budget without buying attention.

    From grayscale with alerts: "Is anything permanently coloured, and has anyone noticed?"

  • kpi.context-required agent

    A tile carries at least one context signal beside the value, and the age of the data. Both are required props, so a tile that cannot supply them is a plain figure rather than a KPI wearing the costume of one.

    From kpi tile: "Does the tile carry a delta, a state colour or a trend? Without one of the three, the number is a decoration."

  • color.direction-lightness-differs ci

    --direction-up and --direction-down differ by at least 12 in CIE L*, so direction survives greyscale and the common colour vision deficiencies. Roughly one man in twelve cannot separate them by hue.

    From red green direction: "Do the two colours differ in lightness as well as hue?"

  • color.direction-survives-greyscale ci

    --direction-up and --direction-down stay at least 12 L* apart after grayscale(1). --direction-flat is exempt: it is achromatic, so it cannot separate from anything by desaturation, and the glyph carries it. That exemption is why the glyph is not optional.

    From red green direction: "Does the display survive being viewed in greyscale?"

  • color.direction-not-sentiment agent

    Tokens are named for direction, never for sentiment. Churn rising is not good news, and a token called --positive would have to lie about it.

    From red green direction: "Is 'up' unambiguously good or bad for this metric, and does the colour reflect that rather than the sign?"

  • color.direction-inverted-markets human

    The mapping is a setting. Several markets read red for up, and a product that hard-codes the western convention is wrong in those markets rather than merely unfamiliar.

    From red green direction: "Is the up/down colour mapping a user preference for markets that invert it?"

  • color.status-set-closed ci

    Status is one of nominal, warn, critical or unknown. No other value may be styled as a status, and no raw palette colour may stand in for one.

    From semantic status color: "How many states, and is the set closed and written down?"

  • color.status-nontext-contrast ci

    Every --status-* value meets 3:1 against --background in every theme. WCAG 2.2 SC 1.4.11, because a status indicator is a non-text element carrying meaning.

    From semantic status color: "Do the colours meet contrast requirements against both light and dark backgrounds?"

  • color.status-disjoint-from-series ci

    No --status-* value sits within ΔE00 10 of any --chart-* value. A series colour that reads as a severity is the failure this pattern warns about.

    From semantic status color: "Does any of these colours also appear decoratively or as a series colour on the same page?"

  • color.status-survives-greyscale ci

    The three chromatic severities stay at least 12 L* apart after grayscale(1), measured with the Rec.709 luma the CSS filter actually uses rather than the colour's CIE L*, which is a different number. --status-unknown is exempt and deliberately so: it is achromatic, it stays grey under any filter, and it is separated by its icon and label instead. That is the point of requiring a non-colour signal.

    From semantic status color: "Does the display survive being viewed in greyscale?"

  • color.status-lightness-separated ci

    nominal, warn and critical differ from each other by at least 12 in CIE L*, so severity survives greyscale and the common colour vision deficiencies without relying on the icon alone.

    From semantic status color: "Does the display survive being viewed in greyscale?"

  • color.status-has-nontext-signal agent

    A status colour never appears without an icon, glyph or text label beside it. In the reference implementation this is structural: STATUS_META carries the icon, so the component cannot render a status without one.

    From semantic status color: "Is there a non-colour signal for every state—icon, shape, label, position?"

  • color.status-thresholds-owned human

    The rule behind each colour has a named owner and one place it is defined. A colour is a claim that a rule fired, and a stale rule makes the page confidently wrong in its most trusted channel.

    From semantic status color: "Where are the thresholds defined, and who can change them?"

  • color.seq-monotonic ci

    The sequential ramp rises or falls in CIE L* at every step, with at least 10 L* between neighbours. A ramp that reverses invents a boundary the data does not have, and one that plateaus reads as banded.

    From sequential and diverging scales: "Does the ramp vary monotonically in lightness?"

  • color.div-symmetric ci

    The two arms of the diverging scale match each other in lightness at every step. An asymmetric ramp reads as though one direction matters more, which is a claim the data has not made.

    From sequential and diverging scales: "If diverging, does the midpoint sit at a real zero rather than at the mean?"

  • color.scale-clamped human

    The domain is clamped at a stated percentile rather than at the extremes, and the legend says so. One outlier otherwise takes the whole ramp and every other value collapses into the first step.

    From sequential and diverging scales: "Is the scale clamped, and does one outlier otherwise consume it?"