Tokens and themes
Every visual decision is a custom property prefixed --iv-, generated from one JSON source. Override any of them on :root or on a scope; nothing needs recompiling.
:root {
--iv-color-primary: #4338CA; /* your brand */
--iv-color-on-primary: #FFFFFF;
--iv-radius-md: 0.25rem;
}
.checkout { --iv-button-radius: var(--iv-radius-full); } /* component-local */Themes
data-iv-theme accepts light (default when absent), dark and system. Scopes nest and the nearest explicit value wins; system follows prefers-color-scheme without JavaScript or storage.
<div class="iv-grid iv-u-gap-4 iv-u-grid-cols-1 iv-u-md-grid-cols-3">
<section class="iv-card" data-iv-theme="light"><div class="iv-card__body"><h3 class="iv-card__title">Light scope</h3><button class="iv-button iv-button--primary" type="button">Primary</button></div></section>
<section class="iv-card" data-iv-theme="dark"><div class="iv-card__body"><h3 class="iv-card__title">Dark scope</h3><button class="iv-button iv-button--primary" type="button">Primary</button>
<section class="iv-card iv-u-mt-4" data-iv-theme="system"><div class="iv-card__body"><p class="iv-u-m-0">System inside dark follows the OS.</p></div></section>
</div></section>
<section class="iv-card" data-iv-theme="system"><div class="iv-card__body"><h3 class="iv-card__title">System scope</h3><button class="iv-button iv-button--primary" type="button">Primary</button></div></section>
</div>The optional theme module adds getTheme(), setTheme(theme, { persist }), resolveTheme() and restoreTheme(), and dispatches iv:themechange on document. Persistence is opt-in. To avoid a flash before the stored theme applies, add a tiny inline script in <head>; with a strict CSP allow it by hash or nonce.
Semantic colours
| Token | Light | Dark |
|---|---|---|
--iv-color-bg | #FFFFFF | #04070F |
--iv-color-surface | #F5F7FC | #070D1C |
--iv-color-surface-raised | #FFFFFF | #0C1330 |
--iv-color-text | #0B1230 | #EEF2FB |
--iv-color-text-muted | #4F5B7A | #A8B3CC |
--iv-color-border | #D3DAEA | #33406A |
--iv-color-border-strong | #6B7896 | #6B7896 |
--iv-color-primary | #1D4FC4 | #66B1FF |
--iv-color-on-primary | #FFFFFF | #06122B |
--iv-color-primary-hover | #1A3FA8 | #8FC6FF |
--iv-color-primary-active | #172F7A | #3D94FF |
--iv-color-primary-subtle | #EAF4FF | rgb(102 177 255 / 0.12) |
--iv-color-accent | #0B6B8A | #73DFFF |
--iv-color-on-accent | #FFFFFF | #06122B |
--iv-color-success | #4F7A12 | #8BBF3A |
--iv-color-on-success | #FFFFFF | #06122B |
--iv-color-success-subtle | #F1F8E4 | rgb(139 191 58 / 0.12) |
--iv-color-danger | #C22E26 | #FF9AA4 |
--iv-color-on-danger | #FFFFFF | #06122B |
--iv-color-danger-subtle | #FFF1F0 | rgb(255 154 164 / 0.12) |
--iv-color-warning | #8F5C00 | #FBBF24 |
--iv-color-on-warning | #FFFFFF | #06122B |
--iv-color-warning-subtle | #FFF8E6 | rgb(251 191 36 / 0.12) |
--iv-color-info | #3D47C2 | #8B95FF |
--iv-color-on-info | #FFFFFF | #06122B |
--iv-color-info-subtle | #EEF0FF | rgb(139 149 255 / 0.12) |
--iv-color-focus | #1D4FC4 | #73DFFF |
--iv-color-overlay | rgb(11 18 48 / 0.55) | rgb(2 5 12 / 0.78) |
--iv-color-hover-surface | rgb(11 18 48 / 0.04) | rgb(230 240 255 / 0.05) |
--iv-color-primary-border | rgb(29 79 196 / 0.55) | rgb(102 177 255 / 0.5) |
Contrast targets: text 4.5:1, controls and focus 3:1 on their surfaces. The values above were checked against the WCAG formula; results are re-measured on each release.
The expressive layer
Six tokens added in v0.6. They name the recipes the polish pass had repeated by hand across a dozen modules — a shadow, a hover tint, a brand edge, a focus halo and two letter spacings — so every module now reads the same value from one place. Nothing changed visually when they landed: they are names for what was already drawn.
| Token | Light | Dark | Where it is used |
|---|---|---|---|
--iv-shadow-ambient | 0 2px 10px rgb(11 18 48 / 0.10) | 0 0 1px 1px #1C2544, 0 2px 10px rgb(0 0 0 / 0.5) | Resting elevation of card, wrapped table, alert, accordion and button. One token instead of five recipes. |
--iv-color-hover-surface | rgb(11 18 48 / 0.04) | rgb(230 240 255 / 0.05) | Pointer tint on card, dialog and drawer footers, tabs and the picker field. Table and menu rows keep the brand tint of primary-subtle. |
--iv-color-primary-border | rgb(29 79 196 / 0.55) | rgb(102 177 255 / 0.5) | Brand outline: secondary button, field hover, pagination, and anywhere an edge should read as the brand. |
--iv-focus-halo | 0 0 2px 3px rgb(29 79 196 / 0.35) | 0 0 3px 2px rgb(102 177 255 / 0.32) | The glow that accompanies the focus ring on fields, buttons, pagination, breadcrumbs, stepper markers and the navbar toggle. Always next to the outline, never instead of it. |
--iv-tracking-tight | -0.011em | -0.011em | Letter spacing of card, dialog, drawer and stat titles. |
--iv-tracking-caps | 0.06em | 0.06em | Letter spacing of small caps: table headers, fieldset legends, megamenu and palette headings, stat labels. |
The two colours also appear in the table above, because they are semantic colours like any other. All six are plain custom properties: override them on :root or on a scope and every component that reads them follows.
Scales
| Family | Tokens |
|---|---|
| Space | --iv-space-0 … 16 (0.25rem steps up to 1.5rem, then 2, 2.5, 3, 4rem) |
| Type | --iv-text-xs … 4xl, --iv-leading-*, --iv-weight-*, --iv-font-sans|mono |
| Shape | --iv-radius-sm|md|lg|full, --iv-border-width, --iv-shadow-1|2|3 |
| Focus | --iv-focus-width, --iv-focus-offset, --iv-color-focus |
| Motion | --iv-motion-fast|base|slow (120/180/220 ms; 0 ms under reduced motion), --iv-ease-out |
| Layers | --iv-z-dropdown|drawer|dialog|toast |
| Measures | --iv-measure (68ch), --iv-container-max (75rem), --iv-content-max (90rem) |
| Breakpoints | sm 30em · md 48em · lg 64em · xl 80em · 2xl 90em. Resolved at build time; not available as custom properties inside media queries. |