CanonCSS
GitHub
Menu

For LLMs

Canon's documentation is a technical artifact: the same reference a human reads is injected into a model so it generates consistent markup.

How to use

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.

system-prompt.txt

~1.7k tokens
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.

system-prompt-full.txt

~3k tokens

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">&#10005;</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