Vibrant Gio is a generative design system for Gio desktop
applications. The entire colour system derives from one brand seed — currently
#723ad4 — through theme/tokens.FromSeed: every colour role carries a
nine-step functional ramp (100–900) on a shared lightness scale, with the
role's base pinned separately so the brand colour is reproduced exactly, and
dark mode is the paired ramp — the same step keeps the same job in both
modes, so nothing is assigned twice. The contrast gate is APCA
(step 900 reaches |Lc| 90 and step 700 |Lc| 60 over the 100/200 grounds, every
pin's on-colour |Lc| 60 over its pin); WCAG 2 ratios are reported alongside but
do not gate.
This directory is generated by theme/export (cmd/vg-tokens); do not edit
it by hand — regenerate it.
styles.css— the token sheet: one:rootblock (light colours and elevation levels plus every mode-invariant scale, comfortable density), one.darkclass override block (the paired dark colours and levels) and one.compactclass override block (the compact density metrics only). Addclass="dark"to the root element to switch modes,class="compact"to any subtree to densify it; the two switches are orthogonal. Below the token blocks sits the component class layer (.btnand its modifiers) — see Component classes.theme.json— the generative parameters; see the reproducibility contract below.foundations/color.html— every ramp, pin, step purpose and measured APCA Lc / WCAG ratio per text pair, in both modes.foundations/type.html— every type role at its real metrics.fonts/— the faces behind--font-familyand--font-family-code, self-hosted with their licences: the same Roboto regular and medium the Gio applications embed, and Roboto Mono from the font repository.foundations/layout.html— the spacing scale, the control metrics at both density settings, the radius scale and tonal elevation as rendered specimens.components/*.html— hand-authored component pages (not generated): every class-layer component in every register and state, at both densities, as copyable markup.buttons.htmlcovers.btnand.badge,forms.htmlthe native-element form controls.
Each page reads only from styles.css — every styled value is a var(--…)
reference — and carries a light/dark toggle. Annotation numbers are printed for
both modes, labelled L and D.
| Family | Tokens | Notes |
|---|---|---|
--color-neutral-<step> |
--color-neutral-100 … --color-neutral-900 |
the neutral ramp; steps in hundreds |
--color-primary-<step> |
--color-primary-100 … --color-primary-900 |
the primary ramp; steps in hundreds |
--color-secondary-<step> |
--color-secondary-100 … --color-secondary-900 |
the secondary ramp; steps in hundreds |
--color-tertiary-<step> |
--color-tertiary-100 … --color-tertiary-900 |
the tertiary ramp; steps in hundreds |
--color-error-<step> |
--color-error-100 … --color-error-900 |
the error ramp; steps in hundreds |
--color-success-<step> |
--color-success-100 … --color-success-900 |
the success ramp; steps in hundreds |
--color-warning-<step> |
--color-warning-100 … --color-warning-900 |
the warning ramp; steps in hundreds |
--color-info-<step> |
--color-info-100 … --color-info-900 |
the info ramp; steps in hundreds |
| pins & semantic layer | --color-bg, --color-surface, --color-text, --color-divider, --color-inverse-surface, --color-on-inverse-surface, --color-highlight, --color-accent, --color-on-accent, --color-accent-hover, --color-accent-pressed, --color-secondary, --color-on-secondary, --color-tertiary, --color-on-tertiary, --color-error, --color-on-error, --color-success, --color-on-success, --color-warning, --color-on-warning, --color-info, --color-on-info, --color-error-container, --color-on-error-container, --color-success-container, --color-on-success-container, --color-warning-container, --color-on-warning-container, --color-info-container, --color-on-info-container, --color-error-on-inverse, --color-success-on-inverse, --color-warning-on-inverse, --color-info-on-inverse, --color-badge-neutral-fill, --color-badge-neutral, --color-badge-success-fill, --color-badge-success, --color-badge-warning-fill, --color-badge-warning, --color-badge-error-fill, --color-badge-error, --color-badge-info-fill, --color-badge-info, --color-btn-tonal-fill, --color-btn-tonal, --color-btn-tonal-fill-hover, --color-btn-tonal-hover, --color-btn-tonal-fill-active, --color-btn-tonal-active, --color-control-border, --color-card-border, --color-dialog-border, --color-popover-border, --color-focus-ring, --color-focus-ring-on-accent |
pinned bases, their on-colours, the ramp-resolved surface/divider, the inverse pair the counterpart scheme's ramp resolves, the reserved highlighter no status hue may serve, and the edges and rings each ramp measures for itself — a resting border per level, for the page, the outlined card, the dialog and the popover, and one focus ring for the whole scheme, measured against every level at once, plus the accent fill a filled button's ring lies on, which belongs to no level at all |
--font-family |
--font-family |
the typeface every prose role uses |
--font-family-code |
--font-family-code |
the monospace typeface the code role uses |
--font-<role>-* |
roles display-large, display-medium, display-small, headline-large, headline-medium, headline-small, title-large, title-medium, title-small, label-large, label-medium, label-small, body-large, body-medium, body-small, code; each with -size, -line-height, -weight, -tracking |
px sizes, CSS numeric weights; code is the mono style outside the MD3 grid, at body-medium's metrics |
--density-<metric> |
--density-control-height, --density-chip-height, --density-padding-x, --density-padding-y, --density-min-hit-target |
control metrics, px; :root is comfortable, .compact overrides all but the hit-target floor |
--space-<key> |
--space-0, --space-1, --space-2, --space-3, --space-4, --space-5, --space-6, --space-8, --space-10, --space-12, --space-16, --space-20, --space-24 |
the 4-pt spacing grid, px |
--radius-<key> |
--radius-none, --radius-sm, --radius-base, --radius-md, --radius-lg, --radius-xl, --radius-2xl, --radius-3xl, --radius-full |
corner radii, Tailwind naming, px |
--elevation-<level> |
--elevation-backdrop, --elevation-chrome, --elevation-0, --elevation-1, --elevation-2, --elevation-3 |
tonal surface fills — the DEFAULT elevation cue; ordered from the backdrop up toward the reader, and resolved per scheme, so both blocks state their own six |
--elevation-<level>-<state> |
--elevation-backdrop-hover, --elevation-backdrop-active, --elevation-chrome-hover, --elevation-chrome-active, --elevation-0-hover, --elevation-0-active, --elevation-1-hover, --elevation-1-active, --elevation-2-hover, --elevation-2-active, --elevation-3-hover, --elevation-3-active |
each level's own interaction walk — what a control with no fill of its own takes the surface under it to when hovered or pressed. Taken FROM the level's fill rather than named as a ramp step, because a level is not a ramp step in both schemes |
--shadow-<level> |
--shadow-backdrop, --shadow-chrome, --shadow-0, --shadow-1, --shadow-2, --shadow-3 |
dp box-shadows — the OPT-IN cue for floating transients (menus, dialogs, tooltips) layered over the tonal fill; resting surfaces use the fill alone |
--ease-<name> |
--ease-standard, --ease-standard-accelerate, --ease-standard-decelerate, --ease-emphasized, --ease-emphasized-accelerate, --ease-emphasized-decelerate |
MD3 easing presets as cubic-bezier(); emphasized is the documented single-bezier stand-in for MD3's two-segment path |
--duration-<stop> |
--duration-x-fast, --duration-fast, --duration-normal, --duration-slow, --duration-x-slow |
MD3-pinned duration stops, ms; the reduce-motion variant zeroes them |
| interaction states | --focus-ring-width, --state-disabled-opacity |
the ring's 2 px stroke and the disabled fade fraction for color-mix() — both mode-invariant, unlike the ring's colour, which is measured against a ground that flips and so sits with the colours above |
| scrim | --color-scrim |
the modal backdrop dimmer: translucent black, identical in both modes — a scrim dims by reducing luminance, so it never flips with .dark. The alpha is the sRGB-compositing equivalent of the Gio pattern's 50% linear-space black |
| Step | Job |
|---|---|
| 100 | tinted fill · app ground |
| 200 | tinted fill · one step off the app ground |
| 300 | hover · subtle border, separator |
| 400 / 600 / 800 | intermediate steps; interaction states walk through them |
| 500 | mid-value reference · strong border |
| 700 | low-contrast text (Lc ≥ 60) · pressed |
| 900 | high-contrast body text (Lc ≥ 90) · pressed |
The pinned base — not a ramp step — is the solid fill; hover and pressed on a solid walk one and two steps from the pin toward 900.
styles.css ends with the component class layer, defined over the tokens
above — no literal colours anywhere, the only literal lengths being the
component constants the Gio side also hardcodes (the 20 dp checkbox/radio
glyph, its 10 dp dot, the 16 dp dropdown chevron, the 1/2 dp input
borders) — so it re-brands, flips to .dark and densifies to .compact
with the sheet. Every pointer/keyboard state rule carries a forcing twin
class (.is-hover, .is-active, .is-focus, .is-checked) grouped into
the same rule, so a static page can show a state with exactly the live
declarations; disabled is forced with the native attribute.
.btn is the button, filled by default: the accent pin under its
on-colour. Two modifier classes select the quieter emphasis variants —
.btn.tonal (the accent's tint, --color-btn-tonal-fill under
--color-btn-tonal: the same recipe .badge wears, one hue at two
strengths) and .btn.ghost (no ground at rest; neutral 700 text).
Interaction states resolve as the step walks above: hover walks one step
(:hover), pressed and selected two (:active, .selected); a filled
button's solid fill walks via the emitted --color-accent-hover /
--color-accent-pressed stops, and a tonal button through its own
-hover and -active pairs, whose foreground moves with the fill.
Keyboard focus (:focus-visible) keeps the resting fill and draws
the ring: --focus-ring-width of --color-focus-ring, the one ring the
scheme carries — the step of the primary ramp nearest its mid-value step
that reaches 3:1 against every level at once, so a control wears the
same ring wherever it is put. It also parts from every resting border
in luminance rather than in hue alone, so focus stays findable where a
display or a system setting takes the colour away, and it is never the
accent fill itself, which is what a checked control already paints.
--color-focus-ring-on-accent is the sole exception, for the ring a
filled button insets in its own fill: that
fill is a step of the primary ramp too, and the scheme's ring cannot
read on it. Same ring, same width, same 3:1 floor in every variant.
Disabled (:disabled) fades each colour to
--state-disabled-opacity of its alpha. A ghost has no selected
treatment: it stays quiet. .btn.icon is the icon-only form: a square
the density's control height on a side, the glyph (an inline SVG on
currentColor) inset by the density's vertical padding.
.badge is the inline annotation: label-medium text over a tinted
field of its own hue. One hue at two strengths — a pale fill for the
field and the same hue at reading strength for the word — and never the
inverted pairing, which is what .btn uses and what a badge must not
claim to be. No boundary and no vertical padding, so its height is the
role's line box; the side padding is --space-2 and the corner is
--radius-base, deliberately not the pill .chip wears. The default
is the plain category label; .badge.success / .badge.warning /
.badge.error / .badge.info are the four statuses, differing in hue
alone. Both halves are tokens because both are derived against a ground
rather than named on a ramp — the fill against the page, the foreground
against the fill. Compose them for status; never inline-style a status
colour. A badge is read, not used: no interaction states.
The form controls dress native elements — no script anywhere:
.input (text <input>, and <select class="input select"> inside a
.select-wrap for the chevron), .checkbox and .radio on their native
input types with appearance: none. They resolve exactly as
components/input does: Surface ground under body-large text,
--color-control-border on the resting edge of all four controls,
neutral 700 placeholder and chevron, focus promoting the border to the
ring (2 dp on the text field, the shared outline on checkbox/radio),
disabled fading every colour via color-mix(). That border is the
neutral rung the ramp measures as reaching 3:1 against the window
ground, which is 600 in the light scheme and 500 in the dark; the named
rung it replaced read below the floor in one of them, at 2.67:1 in the
scheme most people read in. The edge follows the control into a raised
host: a surface that fills a deeper level declares --ground-border
beside its own fill, the rules name it with the paper's own token as
the fallback, and every control inside re-derives — the same walk
against the same fill the host measures its own outline against, which
is why a checkbox in a dialog wears the dialog's edge. In the dark
scheme the page's own rung reads 2.62:1 over a level-2 fill and 1.80:1
over a level-3 one, both under the floor; in the light scheme it clears
every level and the handed-down token repeats. The ring does not
follow, because it never left: it is measured against every level at
once and is one colour for the scheme.
Checked, the box is the accent fill under a check mark in the
on-accent pin, drawn from the icon set's grid as two gradient bands — a
fill says a colour was applied and only the mark says what it means. The
radio's selected state is the accent ring and 10 dp dot around a Surface
gap.
Elevation is tonal, and it climbs toward the light: in both schemes, every level is lighter than the one beneath it. One perceptual rule, no second rule for the dark scheme and no mirror — a surface nearer the viewer catches more light, and reflectance does not invert when the room goes dark. Six levels, counted from the backdrop up toward the reader:
| Level | What wears it |
|---|---|
--elevation-backdrop |
nothing: the bare window plane, showing wherever nothing stands; the window's darkest region |
--elevation-chrome |
the window's furniture — navbar, toolbar, sidebar, inspector, status bar, pane |
--elevation-0 |
the content surface, the bg pin |
--elevation-1 |
raised on the content — cards, code fences, text fields |
--elevation-2 |
floating — dialogs, toasts |
--elevation-3 |
floating, nearest the scheme's light extreme — menus, popovers, tooltips |
Read that down and the fill gets lighter, in :root and in .dark
alike. Chrome is window-scale only: the trim inside a component or a
pattern — a card's header, a dialog's footer — is that thing's
structure and takes no level of its own. The chrome level is the one
the ramp does not place: its step under the content is measured off the
platform rather than derived, and the two schemes measure
differently — about 4.9 L* under light content, about 1.5 under dark,
where a full ramp step would read as a hole rather than as furniture.
That asymmetry is the platform's own, not a rule mirrored between the
schemes. The backdrop takes that step scaled by the ramp's own
proportion, because no platform capture shows a window plane beneath
its furniture to measure. A chrome region that floats is still chrome
and still fills at the chrome level: what says it is a floating object
is its own hairline edge and its shadow, never a lighter fill.
The levels stop at 3: desktop has no six-deep stack. Note the
sizes — a light scheme has spent almost all of the tonal axis on its
content, so its levels above the content are separated by a fraction of
an L* and the derived hairline (--card-border, --dialog-border,
--popover-border) is what says where a surface is. That is what the
desktop applications this system is judged against measure too.
The surface fill is the default cue. The dp shadow is the opt-in
secondary cue, reserved for floating transients — menus, dialogs,
tooltips — which layer --shadow-N over their tonal fill. Resting
surfaces never cast one.
Two published settings, one variable family: comfortable (36 dp controls,
32 dp chips, 16/8 dp padding) is the :root default; compact (28 dp
controls, 24 dp chips, 12/6 dp padding) is the .compact class override,
scoping to any subtree the way .dark scopes colours. The chip height is
the control height less 4 dp in both settings — one relation, not a second
scale. --density-min-hit-target (44 dp, WCAG 2.5.5) is deliberately not
overridden: compact shrinks the drawn control, never the clickable area.
theme.json records both settings' metrics plus which one the theme runs.
The MD3 easing presets are emitted as cubic-bezier() variables and the
five duration stops in ms, each pinned to one MD3 duration role. The spring
presets (default/snappy/gentle) are Go-side damped-oscillator physics with
no CSS counterpart, so they live only in theme.json's motion parameters.
Under the OS reduce-motion preference the theme emits the same scale with
every duration zeroed; the sheet is generated from the non-reduced scale.
theme.json records the seed (hex plus its OKLCh hue and sat), the pinned role
hexes per mode, the faces, the base radius, the measured lightness scales, both
density settings' metrics, the elevation model (surface steps and shadow dps
per level) and the motion set (durations, easings, springs).
FromSeed(seed) regenerates every ramp and pin from the seed alone — a
round-trip test in theme/export asserts it — so the file, not this text,
is the contract. To rebrand, rerun vg-tokens -seed #rrggbb; every page here
reflows because nothing in them is hard-coded.
Fonts: the tokens name Roboto (and Roboto Mono for code); the pages fall
back to system faces when they are not installed. An increased-contrast palette variant exists Go-side
(tokens.FromSeedHighContrast, driven by the OS contrast preference); it is
not part of this export.