The one rule that matters most
A value in Malevich is never a literal. It is a token, referenced as
var(--token-name). The only place raw values live is the foundations
source. Everywhere else — components, applications, modifiers — consumes
tokens. The malevich/no-raw-values lint rule enforces this.
If you're about to type a hex code, a pixel value, or a named color into a component or screen, stop and find the token instead.
The three tiers
Tokens form three layers, each referencing only the layer beneath it
(T3 → T2 → T1). Strict separation is non-negotiable under v1.0
(ADR-0008). Read llm-wiki/architecture.md for the full model.
Tier 1 — foundations
Raw, unopinionated primitives: --palette-*, --scale-radius-*,
--scale-spacing-*, --font-size-*, --font-weight-*,
--motion-duration-*. They are the alphabet, devoid of meaning, and
they live in packages/core/tokens/foundations.json.
You never consume foundations directly. Writing
background: var(--palette-accent) in a component is an architecture
violation, caught by malevich/no-foundation-direct-reference.
Foundations exist only to be referenced by semantic tokens.
Tier 2 — semantic
Role-based tokens that carry meaning. This is the public vocabulary
— the language you work in 95% of the time. Defined in
packages/core/tokens/semantic.json. A semantic token's name describes
what it is for, never what it is: --color-accent is right,
--color-red is wrong.
Tier 3 — component-specific
Per-component derived tokens (--tab-background-active,
--tab-text-active), in packages/core/tokens/component-specific.json.
A refinement layer, not a replacement — most components need few or
none. Components consume tier-3 where it exists, otherwise tier-2.
Tier-3 references tier-2; never tier-1.
tier 3 (component-specific) → consumed by components
↓ references
tier 2 (semantic) → consumed by components, apps, modifiers
↓ references
tier 1 (foundations) → referenced by tier 2 only
↓ generated from
canvas.md → the project's taste, in markdown
The semantic vocabulary
These are the tier-2 families you'll reach for. (CSS custom property
names are the dotted token paths with dashes — color.ink-strong
becomes --color-ink-strong.)
color — role, never appearance:
- Ink (text):
--color-ink-strong,--color-ink-regular,--color-ink-subtle,--color-ink-inverse - Surface:
--color-surface-canvas(page),--color-surface-raised(cards, dialogs),--color-surface-float(popovers, tooltips),--color-surface-overlay(modal backdrop), plus--color-bone - Border:
--color-border-default,--color-border-strong,--color-border-muted - Accent:
--color-accent,--color-accent-strong,--color-accent-muted - Status:
--color-success,--color-danger,--color-warning,--color-info(there is no--color-status-positive; use the role name)
font — semantic typography roles (ADR-0009), grouped by purpose. Each role names its purpose, not its size:
- Display (fluid via
clamp()):--font-display-hero,--font-display-statement,--font-display-title - Heading:
--font-heading-title,--font-heading-section,--font-heading-subsection,--font-heading-group - Reading:
--font-body-lead,--font-body-regular,--font-body-support,--font-caption - Functional:
--font-overline,--font-eyebrow,--font-code,--font-mono-l,--font-mono-m,--font-mono-s
Each font token is a composite of five properties and expands into
five CSS custom properties. There is no single font: shorthand var.
Set them individually:
.card__title {
font-family: var(--font-heading-title-family);
font-size: var(--font-heading-title-size);
font-weight: var(--font-heading-title-weight);
line-height: var(--font-heading-title-line-height);
letter-spacing: var(--font-heading-title-letter-spacing);
}
radius — --radius-section, --radius-block, --radius-card,
--radius-button, --radius-pill, --radius-field, --radius-tag,
--radius-avatar, --radius-tooltip, --radius-dialog.
space — inset (padding) and gap families, each -s / -m / -l,
keyed by layer: --space-inset-element-* (inside atomic elements),
--space-inset-block-* (inside blocks), --space-inset-section-*
(inside sections), and --space-gap-elements-* / --space-gap-blocks-*
/ --space-gap-sections-* for the gaps between them.
shadow — --shadow-flat, --shadow-raised, --shadow-float,
--shadow-overlay.
motion — durations --motion-fast / --motion-base /
--motion-slow and easings --motion-easing-default /
--motion-easing-in / --motion-easing-out.
Plus --size-control-{s,m,l,xl} for control heights and
--border-width-{hairline,focus,emphasis}.
How to pick a token
Choose by role and intent, not by how it looks:
- "Text that's a caption" →
--color-ink-subtle, not "the grey one." - "An error message's color" →
--color-danger, not#d32f1e. - "The padding inside a card" →
--space-inset-block-m, not16px. - "A card's corner" →
--radius-card, not4px.
If no semantic token fits cleanly, that's a signal — either the design
intent is unclear, or the system is genuinely missing a token. Do not
invent a token mid-task. Use the closest existing one and flag the gap
for a human to decide. New semantic tokens are a deliberate change to
semantic.json, generated from canvas.md.
Theming, light, and dark
A theme is a set of foundation values satisfying the semantic contract.
Light and dark are not separate themes — they are variants of every
theme, selected by the data-color-scheme attribute on :root. Because
components consume semantic tokens (which resolve differently per
scheme), the same component CSS renders correctly in both modes with no
component changes. Foundations and semantic tokens are generated from a
project's canvas.md by @malevich/canvas; changing the canvas
re-flows every visual surface without touching component code. To shape
that taste from a reference, see the malevich-canvas-import skill.
Output checklist
- No raw colors, sizes, or shadows — every value is
var(--token) - Only semantic (tier-2) or component-specific (tier-3) tokens
consumed; never
--palette-*/--scale-*(tier-1) directly - Tokens chosen by role/intent, not appearance
- Font usage sets all five
--font-<role>-*properties - No new tokens invented mid-task; gaps flagged for a human
- Works in both light and dark (semantic tokens, not mode-specific literals)