Open source · MIT · v0.6.0
A closed styling vocabulary plus a linter that fails the build when you leave it. Humans remember conventions. Agents do not, and there are more of them every month.
35.7kb raw, 6.2kb gzipped, zero JavaScript, zero build step, zero dependencies.
The difference
Markup should say what something is, not carry every styling decision needed to build it. Both of these are real output: the first is what a competent Tailwind house-style prompt produced for a call to action, the second is the same button in Canon.
<a class="inline-flex items-center justify-center rounded-lg
bg-indigo-600 px-4 py-2 text-sm font-semibold text-white
hover:bg-indigo-700 focus:outline-none focus-visible:ring-2
focus-visible:ring-indigo-600 focus-visible:ring-offset-2"><a data-component="button" data-variant="primary">The hover state, the focus ring, the radius, the weight and the padding are the button's job. The colour moved to a theme file, where changing it changes every button at once.
Measured
Regenerate any of these yourself with npm run census and npm run repro. Every figure on this site comes out of a script, not a slide.
The part that lasts
A strict house style written into a prompt produces consistent output. We measured that, and it is in the research below. But the prompt governs one generation. It is not in the repository, the next agent may not receive it, and nothing checks the diff.
Canon puts the same rules in the codebase. The vocabulary is the markup, and canon-lint exits 1 when something leaves it. That is the difference between a convention and a specification: one is remembered, the other is enforced.
Six clean-context agents were asked to give the cards rounder corners. All six wrote border-radius: var(--radius-lg), which is the value the card already had. All six reported the job done.
Nothing rendered differently and the diff looked like work. That is rule R9, and those six files are now its regression test. A reviewer would have approved every one of them.
The hard part of a real codebase
Generating a page once is the easy case. The expensive one is changing an application you already have: a rebrand, an acquisition, a second product on the same system, or a year of small edits by people and agents who never met.
Because the markup says what something is and never how it looks, the identity lives in one file of token overrides. Measured on two deliberately opposite brands: each overrode 49 tokens, only two landed on the same value, and the markup stayed 96% structurally identical.
A utility framework cannot make that trade, because there the brand and the markup are the same artefact. Changing the look means editing every element.
A control group says a closed vocabulary is not the only fix. A strict Tailwind prompt with a written-down house style reproduced as well as Canon on one spec and beat it on another. Canon's case is that it is that written vocabulary, already done, mechanically checked, and a third the size.
The whole study, including the conditions where it comes second, is on the research page.
Checked, not promised
A closed vocabulary is only worth something if a machine can tell when you leave it. Canon ships the tools that do.
data-layout="stak" stops compiling and the editor suggests "stack". One line in your tsconfig.Theming
Canon's default look is warm paper, a Charter serif and vermilion. Nothing you are looking at is warm, serif or vermilion. The markup of this site never changed: a theme file of token overrides did, and the whole framework retargeted to match the logo.
That is the claim a utility framework cannot make, because there the brand and the markup are one artefact. Measured on two deliberately opposite brands: each overrode 49 tokens, only two landed on the same value, and the markup stayed 96% structurally identical.
:root {
--color-brand: #131313;
--color-accent: #0b8074;
--radius-md: 0;
--font-display: ui-monospace, monospace;
--shadow-sm: 2px 2px 0 rgb(19 19 19 / 0.08);
}Five of the fifty-five lines that make this site look like itself.
Install
<link rel="stylesheet" href="canon.css">Or through npm, where it is one import in your root layout and nothing else:
npm install canoncss
import 'canoncss/dist/canon.css'You can also take the code and keep it. npx -p canoncss canon-init copies the stylesheet, a starter theme and an AGENTS.md into your repo, after which you can uninstall the package and nothing breaks. A closed vocabulary should not also be a lock-in.
The rules
data-layout or data-component, never both.data-slot only as a direct child of its parent.@layer canon.app, built from tokens. The linter counts those rules, because that number is what Canon is missing for your product.That is the entire surface, and it is meant to stay small.