HTML first
Native elements wherever they exist: a real <button>, <dialog>, <details>. Meaning lives in attributes, not in wrappers.
How to add Canon to a page, how its markup is read, what needs JavaScript and which browsers it supports. For what Canon is and why, start with the Introduction.
Link Canon's stylesheet (it brings the core with it) and, for behavior, canon.js. There's no build step, so a page can take both from a CDN:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/canon-ui@0.2.1/css/canon/canon.css">
<script src="https://cdn.jsdelivr.net/npm/canon-ui@0.2.1/js/canon.js" defer></script>
Or install it from npm:
npm i canon-ui
Then link the two files from where the package put them, or from wherever your app serves them. Keep the css folder's files together, since a stylesheet finds its icons and fonts next to it:
<link rel="stylesheet" href="node_modules/canon-ui/css/canon/canon.css">
<script src="node_modules/canon-ui/js/canon.js" defer></script>
Then write the markup each component's page shows. A primary action:
<button type="button" class="canon-button" data-emphasis="high">
<span class="canon-icon" data-icon="plus" aria-hidden="true"></span>
<span class="canon-button-label">New invoice</span>
</button>
A field, its label tied to its input:
<div class="canon-field">
<div class="canon-label">
<label class="canon-label-text" for="email">Billing email</label>
</div>
<div class="canon-text-input">
<input id="email" type="email" placeholder="billing@globex.example">
</div>
</div>
Native elements wherever they exist: a real <button>, <dialog>, <details>. Meaning lives in attributes, not in wrappers.
Popovers, dialogs, invoker commands and anchor positioning do the work; canon.js fills only the gaps, and steps back as browsers catch up.
Names, roles, focus and keyboard are part of each component's markup, so a theme can't lose them and an app doesn't have to remember them.
Hints like data-size and data-density sit above the theme in the cascade. The author is never required to say anything; the theme decides the rest.
Logical properties throughout, so right-to-left works as well as left-to-right; every word Canon writes comes from CSS, ready to translate.
Each component says what it does without canon.js and what it adds with it. When a script doesn't load, the page still works.
Every attribute has one job. Knowing which channel says what makes any component's page quick to read.
| Channel | Says | For example |
|---|---|---|
class | What it is, and its parts | canon-button, canon-button-label |
data-* | What it means: its variant | data-emphasis="high", data-intent="danger" |
| ARIA and native attributes | Its state | disabled, aria-pressed, aria-current |
| Hints | What only the author knows about its place: its room, its density, icons alone. The theme decides how that looks | data-size, data-density, data-display |
data-state | State canon.js keeps (rare) | data-state="changed" |
Hints are inherited: put one on a container and everything inside follows, unless it says otherwise.
<div data-size="sm">
…every control in here is small, unless one says otherwise…
</div>
Each component's specification gives its tier, so you know what to expect before you build:
Canon supports current versions of Chrome, Edge, Firefox and Safari. It needs cascade layers, :has(), light-dark(), container queries, popovers and dialogs, which every current browser has (Chrome and Edge 123, Firefox 125 and Safari 17.5 onwards). Newer platform features are used where a browser has them; elsewhere canon.js fills them in, or the page does without:
| Feature | Canon uses it for | Without it |
|---|---|---|
| Anchor positioning | Placing menus, popovers, lists and tooltips next to what opened them | canon.js places them (in browsers older than Chrome 125, Safari 26 or Firefox 147) |
Invoker commands (commandfor) | Buttons that open dialogs and run Canon's commands | canon.js fills them in |
Interest invokers (interestfor) | Tooltips that show on hover and focus | canon.js fills them in |
closedby on dialogs | Closing a dialog with a click beside it | canon.js fills it in |
field-sizing | Fields that grow with what's typed | canon.js sizes them |
Typed attr() and if() | Colors from data-seed, sizes from attributes | canon.js and the stylesheet cover them |
| Customizable select | A Select's list in Canon's look | The browser's own list, which works the same |
hidden="until-found" | Find in page reaching text in a Tab's hidden panel, and choosing its tab | Find in page only searches the panel that shows |
| Scroll-state container queries | A line under a stuck Page Header or over a stuck Save Bar | No line; they stick the same |
These docs say so at the top of a page when your browser is missing one of them.
Each component has a page in three parts:
docs/components/…): elements, attributes, behavior, accessibility, and what a theme must do.Patterns put the components together into whole screens: finding, opening, editing and saving records. Search everything with the top bar's search, or ⌘K.
For developers and theme authors: css/canon.debug.css outlines, in a dashed amber line, any markup on the page that breaks a component's contract: a missing name, a part in the wrong place, a second primary action. Add js/canon.debug.js and each outlined element gets a warning right below it: the contract it breaks, why, and the exact condition it fails. Load both while you build, never in production.
In these docs, turn it on from the search (⌘K, "Turn contract check on") or add ?debug to a page's address.
Canon's styles sit in cascade layers, so your own unlayered CSS always wins, with no specificity fights. To go further, write a theme: one stylesheet that declares Canon's layers, imports css/canon/canon.core.css and draws every component, in place of css/canon/canon.css. Each component's specification lists what a theme must do. The markup never changes, so pages move to a new theme by changing one line. Themes compares the ones that come with Canon.