Canon's documentation is a technical artifact: the same reference a human reads is injected into a model so it generates consistent markup.
Chat UIs - paste the prompt at the top of your conversation, then ask for UI.
API calls - send it as the system message.
Claude Code / Cursor / agents - append it to your CLAUDE.md or rules file. Every generation in the repo then speaks Canon.
CANON CSS v0.1 - write HTML using only this vocabulary.
TOKENS (CSS custom properties - never hardcode a value):
--space-{xs|sm|md|lg|xl|2xl}
--text-{xs|sm|md|lg|xl|2xl|3xl}
--color-{brand|accent|surface|surface-raised|surface-sunken|content|
content-subtle|content-inverse|border|border-strong|success|warning|
error|info} (+ -subtle variants on brand/accent/status colors)
--width-{prose|content|wide}
--radius-{sm|md|lg|full} --shadow-{sm|md|lg} --weight-{normal|medium|bold}
--font-{sans|display|mono}
LAYOUTS - data-layout on containers:
stack vertical flex
row horizontal flex + data-wrap
grid css grid + data-cols={1|2|3|4|auto}
sidebar two-col > data-slot={sidebar|main}
centered centered max-width + data-width={prose|content|wide}
hero full-viewport section
split two equal columns
Any layout also takes:
data-gap={xs|sm|md|lg|xl|2xl}
data-align={start|center|end|stretch}
data-justify={start|center|end|between|around}
COMPONENTS - data-component on semantic elements:
button + data-variant={primary|secondary|ghost|danger|link}
+ data-size={sm|md|lg}
card > data-slot={media|header|body|footer} (media = full-bleed top img)
+ data-variant={featured} to highlight one card in a group
Header text is auto-styled - plain text, no heading tags needed.
badge + data-variant={neutral|brand|success|warning|error|info}
input + data-size={sm|md|lg} + data-state={error|success}
textarea + data-size={sm|md|lg} + data-state={error|success}
select + data-size={sm|md|lg}
topbar > data-slot={brand|nav|actions|menu}. menu = mobile burger,
zero-JS: <details data-slot="menu"><summary>Menu</summary>
<nav>links</nav></details>; auto-hides on desktop and
replaces the inline nav on mobile. The burger is drawn by Canon
and the word you write is hidden, staying as the accessible
name - never put an icon character in there.
modal > data-slot=panel > data-slot={header|body|footer}
avatar + data-size={sm|md|lg}. A <span> with initials, or an <img> inside
the span - never data-component on the <img> itself.
stat the big number of a metric (<span data-component="stat">4.8M</span>)
table on a <table> element; style-free thead/tbody/th/td inside
stepper an <ol> of checkout/onboarding steps. Current one is
aria-current="step"; done ones are data-state="complete";
upcoming is neither. Numbers and rails are generated.
nav a list of navigation links, usually in a sidebar. Mark the
current one with aria-current="page", never a data-* value.
The topbar's own nav slot honours aria-current too.
disclosure on <details>, zero-JS expand/collapse. First child is the
<summary>; the caret is generated. Use for FAQ and any
show-more section.
divider + data-variant={strong}
alert a block-level message. + data-variant={success|warning|error}
(info is the default). Carries no role by itself: add
role="status" if it appears in response to something the user
did, role="alert" only if it must interrupt.
breadcrumb on <nav> wrapping an <ol> of ancestors. Separators are
generated; mark the last one aria-current="page".
UTILITIES - complete list; nothing else exists:
data-padding={xs|sm|md|lg|xl|2xl} data-tone={subtle|brand|accent|success|error}
data-mono data-full data-truncate data-hide={mobile|desktop}
data-motion={rise|float|pulse|lift} (entrance | idle drift | attention |
hover elevation; all auto-respect prefers-reduced-motion)
class="sr-only"
PROSE - styled automatically, no attributes needed:
checkbox and radio inputs render bare - never give them data-component,
and a <label> wrapping its own control lays itself out.
<progress value="60" max="100"> renders bare: the element is already the
role. A switch is <input type="checkbox" role="switch">, also bare, because
the role is the contract - there is no switch component.
ul/ol/li, blockquote, pre/code all render correctly bare. Sidebar/section
labels are <p data-tone="subtle">, never a heading element.
CANONICAL DEFAULTS - use these unless something forces another choice:
A pager is not a component and does not need one. Ten of ten
clean-context generations built the same thing without it, so it is a
canonical construction instead: <nav aria-label="Pagination"
data-layout="row" data-gap="sm" data-align="center" data-wrap> holding
button anchors, ghost for the other pages, primary plus
aria-current="page" for the current one, secondary for Previous and Next.
Spacing: page sections data-gap="xl"; card bodies and field groups
data-gap="md"; buttons sitting side by side data-gap="sm".
A number with a unit: <span data-component="stat">$79</span> then
<span data-tone="subtle">/month</span>. There is no price component.
A control and its label, or a number and its caption, are a stack with
data-gap="xs". Never leave the two as bare siblings.
Highlighting one card in a group: data-variant="featured" on the card
plus a badge as the first child of its header slot.
RULES - enforce strictly:
1. Only defined tokens. Never hardcode colors, sizes, spacing.
2. No inline styles, no <style> blocks, no extra CSS.
3. An element gets data-layout OR data-component, never both.
4. data-slot only as a direct child of its parent layout/component.
5. h1-h6 are already sized by the type scale. Do not restyle them.
6. Dark mode: data-theme="dark" on <html>.
7. Theming: adapt Canon to the brand, never bend the markup. A theme file
may override ANY token on :root (colors, fonts, type scale, radii,
shadows) and may add a small `@layer canon.theme` block for the few
brand details tokens cannot express. Markup stays pure vocabulary.
8. Canon ships zero JavaScript. Interactivity is the consumer's job.
9. Component roles sit on their canonical element: button=<button> or <a>,
badge/avatar/stat=<span>, topbar=<header>, divider=<hr>, modal=<dialog>,
card=<article>/<div>/<a>. Form controls and table use their own tag.
10. If a pattern has no vocabulary, it goes in @layer canon.app in the
consumer's own CSS, built only from tokens. Never inline styles,
never a <style> block, never a hardcoded colour or spacing value.
canon-lint counts those rules: the number is what Canon is missing.
11. A component Canon does not have takes data-x-component, kebab-case,
styled in @layer canon.app from tokens. Its regions are data-x-slot.
Reuse Canon modifiers when Canon has the value you need
(data-gap, data-padding, data-size). When it does not, because a
calendar day is "unavailable" and no closed set predicted that, use
data-x-variant and data-x-state, which take any kebab-case value.
Never data-component or data-slot for something not listed above.
Use the full prompt when output drifts - its anti-pattern section is what corrects drift.
CANON CSS v0.1 - write HTML using only this vocabulary.
Canon is a pure-CSS framework with a closed vocabulary. There is exactly one
correct way to express each pattern. If a value has no token, it does not exist.
TOKENS (CSS custom properties - never hardcode a value):
--space-{xs|sm|md|lg|xl|2xl}
--text-{xs|sm|md|lg|xl|2xl|3xl}
--color-{brand|accent|surface|surface-raised|surface-sunken|content|
content-subtle|content-inverse|border|border-strong|success|warning|
error|info} (+ -subtle variants on brand/accent/status colors)
--width-{prose|content|wide}
--radius-{sm|md|lg|full} --shadow-{sm|md|lg} --weight-{normal|medium|bold}
--font-{sans|display|mono} --leading-{tight|normal|loose}
--ease-default --duration-{fast|normal|slow}
LAYOUTS - data-layout on containers:
stack vertical flex
row horizontal flex + data-wrap
grid css grid + data-cols={1|2|3|4|auto}
sidebar two-col > data-slot={sidebar|main}
centered centered max-width + data-width={prose|content|wide}
hero full-viewport section
split two equal columns
Any layout also takes:
data-gap={xs|sm|md|lg|xl|2xl}
data-align={start|center|end|stretch}
data-justify={start|center|end|between|around}
Responsive is automatic: grid cols 4 collapse to 2 under 1024px, cols 3
under 768px, everything to 1 under 480px; split and sidebar collapse to
one column under 768px.
COMPONENTS - data-component on semantic elements:
button + data-variant={primary|secondary|ghost|danger|link}
+ data-size={sm|md|lg}
card > data-slot={media|header|body|footer} (all optional; media =
full-bleed top image). + data-variant={featured} to highlight one
card in a group. Header text is auto-styled - no heading tags needed.
stat the big number of a metric (<span data-component="stat">4.8M</span>)
table on a <table> element; plain thead/tbody/th/td inside
badge + data-variant={neutral|brand|success|warning|error|info}
input + data-size={sm|md|lg} + data-state={error|success}
textarea + data-size={sm|md|lg} + data-state={error|success}
select + data-size={sm|md|lg}
topbar > data-slot={brand|nav|actions|menu}. menu = mobile burger,
zero-JS: <details data-slot="menu"><summary>Menu</summary>
<nav>links</nav></details>; auto-hides on desktop and
replaces the inline nav on mobile. The burger is drawn by Canon
and the word you write is hidden, staying as the accessible
name - never put an icon character in there.
modal > data-slot=panel > data-slot={header|body|footer}
avatar + data-size={sm|md|lg}. A <span> with initials, or an <img> inside
the span - never data-component on the <img> itself.
stepper an <ol> of checkout/onboarding steps. Current one is
aria-current="step"; done ones are data-state="complete";
upcoming is neither. Numbers and rails are generated.
nav a list of navigation links, usually in a sidebar. Mark the
current one with aria-current="page", never a data-* value.
The topbar's own nav slot honours aria-current too.
disclosure on <details>, zero-JS expand/collapse. First child is the
<summary>; the caret is generated. Use for FAQ and any
show-more section.
divider + data-variant={strong}
alert a block-level message. + data-variant={success|warning|error}
(info is the default). Carries no role by itself: add
role="status" if it appears in response to something the user
did, role="alert" only if it must interrupt.
breadcrumb on <nav> wrapping an <ol> of ancestors. Separators are
generated; mark the last one aria-current="page".
UTILITIES - complete list; nothing else exists:
data-padding={xs|sm|md|lg|xl|2xl} data-tone={subtle|brand|accent|success|error}
data-mono data-full data-truncate data-hide={mobile|desktop}
data-motion={rise|float|pulse|lift} (entrance | idle drift | attention |
hover elevation; all auto-respect prefers-reduced-motion)
class="sr-only"
RENDER BARE - never give these a data-component:
checkbox and radio inputs, and a <label> wrapping its own control, which
lays itself out. ul/ol/li, blockquote and pre/code are styled too.
<progress> is already the role, so it is styled bare: <progress value="60"
max="100"></progress>.
A switch is <input type="checkbox" role="switch">. The role is the contract,
the same way aria-current is; there is no switch component.
CANONICAL DEFAULTS - use these unless something forces another choice:
A pager is not a component and does not need one. Ten of ten
clean-context generations built the same thing without it, so it is a
canonical construction instead: <nav aria-label="Pagination"
data-layout="row" data-gap="sm" data-align="center" data-wrap> holding
button anchors, ghost for the other pages, primary plus
aria-current="page" for the current one, secondary for Previous and Next.
Spacing: page sections data-gap="xl"; card bodies and field groups
data-gap="md"; buttons sitting side by side data-gap="sm".
A number with a unit: <span data-component="stat">$79</span> then
<span data-tone="subtle">/month</span>. There is no price component.
A control and its label, or a number and its caption, are a stack with
data-gap="xs". Never leave the two as bare siblings.
Highlighting one card in a group: data-variant="featured" on the card
plus a badge as the first child of its header slot.
RULES - enforce strictly:
1. Only defined tokens. Never hardcode colors, sizes, spacing.
2. No inline styles, no <style> blocks, no extra CSS.
3. An element gets data-layout OR data-component, never both.
4. data-slot only as a direct child of its parent layout/component.
5. h1-h6 are already sized by the type scale. Do not restyle them.
6. Dark mode: data-theme="dark" on <html>.
7. Theming: adapt Canon to the brand, never bend the markup. A theme file
may override ANY token on :root (colors, fonts, type scale, radii,
shadows) and may add a small `@layer canon.theme` block for the few
brand details tokens cannot express. Markup stays pure vocabulary.
Example theme for an existing brand:
:root {
--color-brand: #0a0a0a; --color-accent: #05e4c2;
--font-display: var(--font-general-sans), sans-serif;
--text-3xl: 4.25rem; --radius-lg: 1.25rem;
}
@layer canon.theme {
[data-component="button"] { border-radius: var(--radius-full); }
}
8. Canon ships zero JavaScript. Interactivity is the consumer's job.
9. Component roles sit on their canonical element: button=<button> or <a>,
badge/avatar/stat=<span>, topbar=<header>, divider=<hr>, modal=<dialog>,
card=<article>/<div>/<a>. Form controls and table use their own tag.
10. If a pattern has no vocabulary, it goes in @layer canon.app in the
consumer's own CSS, built only from tokens. Never inline styles,
never a <style> block, never a hardcoded colour or spacing value.
canon-lint counts those rules: the number is what Canon is missing.
11. A component Canon does not have takes data-x-component, kebab-case,
styled in @layer canon.app from tokens. Its regions are data-x-slot.
Reuse Canon modifiers when Canon has the value you need
(data-gap, data-padding, data-size). When it does not, because a
calendar day is "unavailable" and no closed set predicted that, use
data-x-variant and data-x-state, which take any kebab-case value.
Never data-component or data-slot for something not listed above.
═══ CANONICAL PATTERNS ═══
App shell (topbar + sidebar):
<header data-component="topbar">
<a data-slot="brand" href="/">Logo</a>
<nav data-slot="nav">
<a href="/docs">Docs</a>
</nav>
<div data-slot="actions">
<button data-component="button" data-size="sm">New</button>
<span data-component="avatar" data-size="sm">MA</span>
</div>
</header>
<div data-layout="sidebar">
<aside data-slot="sidebar">
<nav data-layout="stack" data-gap="sm" data-padding="lg">
<a href="/overview">Overview</a>
<a href="/settings">Settings</a>
</nav>
</aside>
<main data-slot="main">
<div data-layout="stack" data-gap="xl" data-padding="xl">
<h1>Overview</h1>
</div>
</main>
</div>
Content page:
<div data-layout="centered" data-width="content">
<div data-layout="stack" data-gap="xl" data-padding="2xl">
<h1>Title</h1>
<p>Body copy.</p>
</div>
</div>
Card grid:
<div data-layout="grid" data-cols="3" data-gap="lg">
<article data-component="card">
<div data-slot="header">Title</div>
<div data-slot="body">Content</div>
<div data-slot="footer">
<button data-component="button">Action</button>
</div>
</article>
</div>
Hero:
<section data-layout="hero">
<div data-layout="stack" data-gap="lg" data-align="center">
<span data-component="badge" data-variant="brand">v0.1</span>
<h1>Headline</h1>
<p data-tone="subtle">Supporting line.</p>
<div data-layout="row" data-gap="sm">
<button data-component="button" data-size="lg">Primary</button>
<button data-component="button" data-variant="secondary" data-size="lg">Secondary</button>
</div>
</div>
</section>
Form:
<form data-layout="stack" data-gap="md">
<label for="name">Name</label>
<input data-component="input" id="name" type="text">
<label for="notes">Notes</label>
<textarea data-component="textarea" id="notes" rows="4"></textarea>
<div data-layout="row" data-gap="sm" data-justify="end">
<button data-component="button" data-variant="ghost" type="reset">Cancel</button>
<button data-component="button" data-variant="primary" type="submit">Submit</button>
</div>
</form>
Field with an error:
<div data-layout="stack" data-gap="xs">
<label for="email">Email</label>
<input data-component="input" id="email" type="email"
data-state="error" aria-invalid="true" aria-describedby="email-err">
<span id="email-err" data-tone="error">Enter a valid address.</span>
</div>
Modal (markup only - you must supply the open/close behaviour):
<div data-component="modal" role="dialog" aria-modal="true" aria-labelledby="t">
<div data-slot="panel">
<div data-slot="header">
<span id="t">Title</span>
<button data-component="button" data-variant="ghost" data-size="sm"
aria-label="Close">✕</button>
</div>
<div data-slot="body">Body.</div>
<div data-slot="footer">
<button data-component="button" data-variant="ghost">Cancel</button>
<button data-component="button" data-variant="danger">Delete</button>
</div>
</div>
</div>
List row (avatar + text + status):
<div data-layout="row" data-gap="md" data-align="center" data-justify="between">
<div data-layout="row" data-gap="sm" data-align="center">
<span data-component="avatar" data-size="sm">JD</span>
<span data-truncate>Jordan D.</span>
</div>
<span data-component="badge" data-variant="success">Active</span>
</div>
═══ ANTI-PATTERNS ═══
WRONG: <div style="display:flex; gap:16px">
RIGHT: <div data-layout="row" data-gap="md">
WRONG: <div data-layout="stack" data-component="card">
RIGHT: <article data-component="card"><div data-slot="body">…</div></article>
WRONG: <h1 style="font-size:3rem">
RIGHT: <h1>
WRONG: <div data-gap="20px"> (not a token)
RIGHT: <div data-gap="lg">
WRONG: <span style="color:#64748b">
RIGHT: <span data-tone="subtle">
WRONG: card slots wrapped in an extra div
RIGHT: data-slot elements are direct children of the card