@canonical/styles

中文文档
0.45.0发布于 2026/10/23LGPL-3.0ES Module构建来源已验证近 30 天下载 1万

Canonical Design System global stylesheet. The reset, typography, and design tokens, in named cascade layers.

原始使用文档
文档目录15 节
  1. Installation
  2. Usage
  3. Entry points
  4. Cascade Layers
  5. What Is Layered Where
  6. What Is Deliberately Unlayered
  7. Where the Element-Level Rules Apply
  8. What This Package Guarantees
  9. What This Package Does Not Guarantee
  10. Browser Floor
  11. Migrating to the Layered Release
  12. What Moves on the Page
  13. Design Tokens
  14. Dependencies
  15. Package Structure

@canonical/styles

This README is written for two readers: the person adding the design system to an application, and the person maintaining this package. Where a claim can be checked by a test, the test is named.

The Canonical Design System's global stylesheet. One import brings in the reset, the typographic engine, the design tokens, the modifier families and the layout presets. Everything this package itself ships is in a named cascade layer, and its element-level rules style the whole page, as a reset does — nothing is confined and nothing has to be marked. The typographic engine, which lives in @canonical/styles-typography and is released with this package, is in the same layers.

Installation

bun add @canonical/styles

Usage

Import once, as the first import of your application's entry point, before any component:

@import url("@canonical/styles");

Or from JavaScript or TypeScript:

import "@canonical/styles";

Then declare the surface on your root:

<html class="app comfortable">

That single line is the root contract, and it has two parts.

ClassWhat it meansValues
contextThe kind of surface, which sets the comfortable/dense pair for every density value.app, site or docs
densityWhich of that pair is in force.comfortable or dense

A colour scheme is optional: light or dark pins one, and leaving both off lets the page follow the reader's operating system. Nothing else belongs on the root — in particular there is no class that switches the reset on. It applies because the stylesheet is loaded.

Both classes set custom properties, which inherit, so they may sit on the document element or on any ancestor of the components that read them — the reference application puts app comfortable on <body>.

ds is not a root class and never has been. It is what every component puts on its own root, so that a component rule can select .ds.button rather than .button and cannot collide with a page's own class names.

A page that also runs another CSS framework cannot let this stylesheet restyle the half it has not migrated, and that is what @canonical/styles-vanilla-adapter is for: it ships a copy of this package's three element-level layers confined to the parts a team has handed over, keeps the two systems out of each other's way, and bridges the theme signal between them. A test in that package binds its copy to the files here so the two cannot drift. Nothing about that arrangement changes this package or the markup of a page that does not need it.

Entry points

Five. @canonical/styles is the one an ordinary page wants: the whole stylesheet, in the order above. Three more are that stylesheet in parts, and the fifth is the order statement on its own, which the other four import.

EntryWhat it is
@canonical/styleseverything: the values, the element rules and the layout presets.
@canonical/styles/tokens.cssevery custom property the design system declares, and not one rule that selects an element. Nothing here changes the page until something reads a value from it, with one exception noted below.
@canonical/styles/elements.cssevery rule the design system applies to a plain element: the reset, the root's baseline, and the typography with its baseline engine.
@canonical/styles/layout.cssthe layout presets — grid, subgrid, responsive, intrinsic and content-flow — which claim those five class names in a page's namespace.
@canonical/styles/layers.cssthe order statement and nothing else: no rule, no import, no declaration. The same statement the four above open with; each imports it as its first rule. A page never needs it on its own.

One exception to "changes nothing", and it is worth knowing before you import tokens.css. The design tokens' generated theme sheet declares color-scheme on :root, .light and .dark alongside the colour tokens, and that property is not inert: it decides the colour of the canvas, the form controls, the scrollbars and the system colours, whether or not anything reads a token, and on a mixed page an element of the other framework's carrying light or dark picks it up too. It cannot be split out here — the sheet is generated and the declarations sit in the same rules as the tokens — so separating them is a change to the tokens package's emitter, tracked as PRA-149. An application that must not have it declares color-scheme itself, in a layer above ds.modifiers or unlayered. Those three declarations are the only non-custom properties tokens.css carries; everything else in it is a custom property, which does nothing until a rule reads it.

The parts exist for one reason. A page that also runs another CSS framework cannot take the element rules: the other framework has its own p rule, and only one of the two can own line-height. Such a page takes tokens.css and layout.css, and gets its element rules from that framework's adapter instead, in a copy confined to the part of the page the design system owns — @canonical/styles-vanilla-adapter builds that copy out of the same files elements.css imports.

Two properties make the parts safe to mix, and both are visible in how the entries are written.

Every entry imports the order first, and the order is written in one file. layers.css holds the statement and nothing else; each of the four entries imports it as its first rule. The order has to reach the browser before any rule it orders, and writing it once means two entries can never disagree — which they could if each repeated the list, and which would leave the same rules arbitrating differently depending on the entry a consumer picked. Measured in Chromium: a statement read through an @import orders the importing sheet exactly as one written in place would.

No entry imports another entry. An @layer statement inside a layer block declares sublayers of that layer rather than top-level layers, so an entry that composed another would nest the order instead of sharing it. Keeping them independent means a page may load one, two or all three, in any order, and get the same result — and that each file is fetched, parsed and applied once, which matters because a browser treats every @import as its own stylesheet and de-duplicates nothing.

Cascade Layers

Everything this package itself ships is in a named layer, and the order is fixed by one statement, which lives in layers.css and which every entry imports before anything else. The typographic engine, which this package imports, is in the same layers:

@layer normalize, ds.tokens, ds.reset, ds.typography, ds.modifiers, ds.surfaces,
  ds.states, ds.components, ds.components.global, ds.components.sites,
  ds.components.documentation, ds.components.stores, ds.components.apps,
  lib, app;

Read it from the bottom up — each position is an argument.

LayerWhat is in itWhy it sits where it does
normalizeThis package's own reset.Lowest, because everything else is meant to overrule it.
ds.tokensThe primitive values, and the spacing, motion and overflow tokens.Above the reset, because a token has to exist before anything reads it; below everything that reads one.
ds.resetThe declarations the document root makes for itself: font, colour, line height, weight, text wrapping, font smoothing — and border-box sizing for it and everything inside.Above the tokens because it reads them; below the typographic engine and the components, which refine what it starts.
ds.typographyThe element rules that apply the typographic mapping, and the baseline engine, from @canonical/styles-typography.Above the reset because it is a more specific statement about text; below the modifiers, which can retune the scale.
ds.modifiersTheme, the typographic scale, the intent families (anticipation, criticality, emphasis, importance) and their shims, and the context and density classes.Above typography, because a modifier's job is to shift what the layers below produced.
ds.surfacesThe surface families: surface, contrasted, modal.Above the modifiers, because a surface re-points colour channels the modifiers set.
ds.statesThe derived hover, active and disabled channels.Above the surfaces, because a state is derived from whatever the surface resolved to.
ds.componentsNothing, by rule. It is the parent of the tier layers below and holds no rule of its own.Highest of the design system's eight top-level layers, so a component is the final word on its own box. A rule written directly into a parent layer sits in that layer's implicit final sublayer, which is above every named sublayer — so such a rule would outrank every tier and no component package could override it by layer. Everything this package puts in ds.components therefore sits in a tier.
ds.components.globalThe stylesheets of the shared component packages, and this package's own layout presets.The base of the tier tree: what every other tier refines.
ds.components.sitesThe stylesheets of the sites tier.The four second-level tiers never appear on the same page, so their order among themselves decides nothing. It is fixed here anyway, so that it can never come to depend on which package a bundler emits first.
ds.components.documentationThe stylesheets of the documentation tier.As above.
ds.components.storesThe stylesheets of the stores tier.As above.
ds.components.appsThe stylesheets of the shared applications tier; one application's own tier declares a layer of its own above it.As above.
libThe stylesheets of libraries that are not part of the design system, such as documentation tooling (@canonical/react-tokens, @canonical/storybook-helpers).Above the design system, so a tool's own rules win over the components it displays.
appThe application's own CSS.Highest, so the application has the last word on its page.

lib and app are the only layer names outside normalize and ds.*. An order statement fixes the relative order of layers the first time they appear, and a later statement can never reorder them, so an application imports this package first and writes its own CSS in @layer app, with no statement of its own.

This section is the reference: what is in each layer, and where. The reasoning behind the order — how a browser decides, why a rule in no layer beats every layered one, why the confinement a mixed page needs lives in the adapter rather than here, and what a bundler does to the statement — is the cascade contract.

The Component Tiers

The component layers follow the design system's tier tree: who owns a component decides which layer its rules go in, and a deeper tier wins. Three rules make that work, and they are worth stating plainly because a package author has to follow them.

The second level is named here, in full. global, sites, documentation, stores and apps are the design system's four second-level tiers plus the shared base, and this statement fixes their order. A package in one of those tiers wraps its stylesheets in its own name and needs to do nothing else.

A sub-tier declares its own layer, and that is what sorts it above. One application's own package — the LXD tier, say — writes its CSS entry in two steps:

@layer ds.components.apps-lxd;       /* 1. name your layer */

@import url("./Button/styles.css");  /* 2. then import your stylesheets */
/* … the rest of the package's sheets, each wrapped in that layer … */

The order statement comes from the application loading @canonical/styles first (see Usage). A layer name is placed the moment the browser first meets it, so with the fifteen names already fixed, ds.components.apps-lxd, being new, is appended after them inside ds.components, where it sorts above every tier. That is the tree's own rule — the more specific tier wins — and it means this stylesheet never has to know which applications exist.

Nothing is ever written directly into ds.components. A declaration in a parent layer sits in that layer's implicit final sublayer, which is above every named sublayer, so a rule written there would outrank every tier — the opposite of what the tiers are for.

The names are tier ids from that tree, lowercased and hyphenated for a sub-tier (apps-lxd, sites-webcomponentsprototype). They are not the context words a page puts on its root (app, site, docs): a tier says who owns a component, a context says what kind of page it is being shown on, and the two are chosen by different people for different reasons.

What Is Layered Where

FileLayerSelects elements?
normalize.cssnormalizeyes
reset.css root declarationsds.resetyes
reset.css box-sizingds.resetyes — *, ::before, ::after
spacing.cssds.tokensno
motion.cssds.tokensno
overflow.css root defaultds.tokensno
overflow.css .surfaceds.surfacesno
grid.css layout presets, and the content-flow containerds.components.globalyes
grid.css :root defaultsds.components.globalno — tokens the presets read, kept with them rather than moved to ds.tokens
modifiers.density.cssds.modifiersno
modifiers.states.shim.css, modifiers.importance.shim.css, modifiers.criticality.shim.cssds.modifiersno
controls.hover.shim.cssds.surfaces and ds.statesno
@canonical/styles-typographyds.tokens for its naming shims, ds.typography for its element rules and engine, ds.modifiers for the typographic scale it importsits element rules, yes; its tokens, no
@canonical/design-tokens distribution filesds.tokens, ds.modifiers, ds.surfaces, ds.states — each file opens its own, except modifiers.importance.css, which is empty and opens noneno

The files marked yes are the ones the adapter's confined copy has to mirror. The rest are almost all custom properties, which do nothing until a rule reads them, and a rule that reads one matches a design-system class. Two exceptions, both deliberate and both in files this package imports rather than writes:

  • modifiers.theme.css sets color-scheme — light dark on :root, light on .light, dark on .dark — document-wide. That is the point of it: the colour scheme is what every light-dark() token resolves against, including the browser's own form controls and scrollbars, and it has to reach them.
  • states.css derives its state channels on * — thirty custom properties on every element. It is a generated file this package does not author, and it declares nothing but custom properties, so its universal selector costs a page nothing it would not pay anyway.

What Is Deliberately Unlayered

One thing: @font-face, in fonts.css. It defines a font for the whole document, not a style for an element, and this stylesheet declares each face exactly once, so no layer has anything to order it against — putting it in one would change no computed value. The file is opt-in and imported separately so that an application already serving the same files does not download them twice.

(Layers do sort @font-face rules where two of them declare the same family: the higher layer's face wins over the later one in source order. There simply are no duplicates here.)

Where the Element-Level Rules Apply

To the whole page. Three of the layers select elements rather than declare custom properties — normalize, ds.reset and ds.typography — and they apply the way a reset always has: the stylesheet is loaded, so the rules are in force. There is no marker to add and no subtree to nominate.

That is a deliberate ruling, not an oversight. An earlier draft of this release confined those layers to a marked subtree so that a page running two CSS frameworks could keep them apart. The confinement works, but it made every ordinary page pay for a problem only some pages have: a class on the root, a browser floor it did not need, and a reset that silently did nothing if the class was forgotten.

The problem is real, so it is solved where it belongs. @canonical/styles-vanilla-adapter ships a confined copy of these three layers, applying only inside the parts of a page a team has handed over, and a test in that package binds the copy to the files here so the two cannot drift. A page that needs it installs it; a page that does not never hears about it.

What This Package Guarantees

GuaranteeThe check behind it
Every rule the package itself ships is in one of the thirteen design-system layers, and the statement reaches the browser before any of them, through the import every entry opens with. The typographic engine it imports is in them too.The order fixtures that arrive with the Vanilla adapter package read the statement out of the resolved stylesheet and check every layer name the package opens against it. A check inside this package — that the set of layers used equals the set declared — is being added separately.
An application tier's rule for a component beats the global tier's rule for the same component, whichever of the two a bundler loads first.The two sublayers are named in the statement, so their order is fixed there rather than at first appearance, and the same order fixture reads it back. The component packages move into them when their stylesheets are wrapped, which is a separate change; until then both sublayers are empty and the guarantee is vacuous.
The package ships no !important.The same fixture file. An important declaration inverts the layer order and cannot be arbitrated by layers at all, so one of them would undo the guarantee above.
Under prefers-reduced-motion: reduce, every motion duration this package defines is 0s.src/motion.css, and the reduced-motion fixture arriving with the adapter package. Zeroing the token is the mechanism: a component reads the token, so nothing has to out-rank the component's own declaration. Whether a given component honours the tokens is that package's guarantee, not this one's. Sheets that still hard-code a duration, and so still animate under the preference, are being moved onto the tokens separately: in the form package the shared input chrome (src/index.css) and eight component sheets (ChoicesField, RichChoicesField, CheckboxInput, RadioInput, ColorInput, ComboboxInput and its list, FileUploadInput, SwitchInput); in the global package Tooltip, ContextualMenu and Popover, which set their own duration property; and one application-tier sheet, the launchpad diff viewer's file header.
The document root declares the baseline — font, colour, line height, weight, text wrapping, font smoothing — and the page inherits from there rather than from the browser's defaults.The computed-style fixtures arriving with the adapter package, which read those properties off every element of a rendered block.

Two guarantees that used to sit here are the adapter's, not this package's: that a region handed over inside a page running another CSS framework computes as it does on an ordinary page, and that nothing reaches a bare element outside such a region. @canonical/styles-vanilla-adapter states and tests both.

What This Package Does Not Guarantee

  • Unlayered application CSS beats every rule here. That is how the cascade is defined: a rule in no layer outranks a rule in any layer. It is not a defect, and it is the escape hatch — but it also means an application that wants this package's layer order to hold for its own CSS has to put that CSS in @layer app. See "Migrating" below.
  • !important in application or third-party CSS beats every rule here, and for important declarations the layer order runs backwards, so the lowest layer wins. Nothing this package can do changes that.
  • This stylesheet styles the whole page. The reset reaches every <body>, <button>, <pre> and <legend> in the document, and the layout presets claim the class names grid, subgrid, responsive, intrinsic and content-flow wherever they appear. That is what a design system's reset is, and it is why a page that also runs another CSS framework needs the adapter rather than this entry point.
  • color-scheme is set on the document root wherever this stylesheet is loaded, because that is what the light-dark() tokens and the browser's own controls resolve against. It is the one thing here a consumer cannot opt out of by not using a class.

Browser Floor

FeatureUsed byChromeSafariFirefox
light-dark()every colour token, including the --color-text the reset declares on the root12317.5120
mod()the baseline engine12515.4118
cap unitthe default baseline engine11817.297

Read the table as a whole, not row by row: the floor is the highest number in each column, because the stylesheet uses all of it. A browser without light-dark() drops the root's color declaration as invalid and falls back to its own text colour, so the reset applies but the page is not themed.

That makes the floor for this stylesheet with its default engine Chrome 125, Safari 17.5, Firefox 120. An application that swaps the default engine for baseline-trim.css raises it to Chrome 133, Safari 18.2, Firefox 154. The typographic engine also uses round(), on the same numbers as mod(); @canonical/styles-typography's "Browser Support" section is the full table, engine by engine.

The design system targets current browsers and does not carry compatibility shims for older ones. An application that cannot move should pin a version.

Migrating to the Layered Release

This release makes two changes that an application has to answer. Neither is markup: there is nothing to add to your root, and the reset applies exactly where it did before.

  1. Your unlayered CSS now beats every rule in this package. Before this release most of what the package shipped was unlayered too, so your overrides competed with it by source order and specificity, and sometimes lost. They no longer can. If you were relying on a rule of ours to win, it will not any more.

  2. Put your own CSS in @layer app. Import this package first, then wrap your application's stylesheet:

    @import url("@canonical/styles");
    
    @layer app {
      /* your CSS */
    }
    

    The package's statement already names app, above every design-system layer, so your rules win by layer rather than by accident of order, and you write no statement of your own.

If you import a subpath rather than the package entry — @canonical/styles/spacing.css and the five other subpaths this package exports — note that a subpath carries no order statement, so the layers it opens are ordered by wherever they first appear in your own stylesheet. Import the entry point unless you have a reason not to.

What Moves on the Page

Measured in Chromium, an application that is the design system's throughout, before against after, with nothing added to the root. Four things change on every element, and one on <hr>; nothing else does.

WhatBeforeAfterWhy
line-height, everywhere it was not set1.15 inherited from the old resetnormalthe document root declares its own baseline
color, everywhere it was not setrgb(0, 0, 0)oklch(0 0 0)the root declares var(--color-text): the same black in the light scheme, but it now follows the theme instead of being the browser's default
box-sizing, everywherecontent-boxborder-boxthe components are authored against it
font-family on <html>the browser's serifthe token stackthe root declares the font; <body> already had it from the typographic mapper
overflow on <hr>visiblehiddenthe browser's own value; a rule has no content to overflow, so nothing shows

Two things that used to be true and are not:

  • The normalize.css package is no longer a dependency. This package writes its own reset, so it can be copied into the adapter's confined build, and so that it contains only the rules the design system actually relies on. If your application imported normalize.css through us and wants the rest of it, depend on it directly.
  • The old README said component styles override modifier styles "regardless of source order". They did not: the layers were declared but almost nothing was written into them. They do now.

Design Tokens

The following token sets from @canonical/design-tokens are included:

Token setContentsLayer it opens
sets.primitiveBase colour palette, spacing scale, font sizesds.tokens
modifiers.themeLight/dark theme mappings, and color-scheme on the document rootds.modifiers
modifiers.surfacesSurface elevation tokensds.surfaces
modifiers.anticipationConstructive/destructive/caution intentsds.modifiers
modifiers.criticalityError/warning/success/information statesds.modifiers
modifiers.emphasisBranded/highlighted/muted emphasisds.modifiers
modifiers.importancePrimary/secondary importance levels — the generated file is empty today, so the shipped mapping is this package's own shim, and a stacked change drops the import until the generated file has contentnone
modifiers.typographyThe typographic scale, through @canonical/styles-typographyds.modifiers
statesInteractive state tokens (hover, active, focus, disabled)ds.states

Dependencies

PackageRole
@canonical/design-tokensCSS custom properties for colour, spacing, and states
@canonical/styles-typographyBaseline grid engine and typographic scale

Package Structure

src/
  index.css                       -- entry point: the layer statement, then the imports
  normalize.css                   -- this package's own reset
  reset.css                       -- the document root's baseline
  spacing.css                     -- spacing tokens
  motion.css                      -- motion tokens, and reduced motion
  overflow.css                    -- scroll overflow affordance
  grid.css                        -- layout presets
  modifiers.density.css           -- the context x density family
  modifiers.*.shim.css            -- temporary shims for unfinished generated modifiers
  controls.hover.shim.css         -- temporary shim for control selected/hover channels
  fonts.css                       -- opt-in @font-face, unlayered

版本与发布记录

@canonical/styles 共发布 57 个版本,首次发布于 2024/11/27;过去 12 个月发布了 39 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.45.0latest2026/10/2317 天前
0.44.12026/10/081 个月前
0.44.02026/10/051 个月前
0.43.02026/10/011 个月前
0.42.02026/09/281 个月前
0.40.02026/09/211 个月前

关于 @canonical/styles 的常见问题

@canonical/styles 是什么?

Canonical Design System global stylesheet. The reset, typography, and design tokens, in named cascade layers.

如何安装 @canonical/styles?

在项目目录运行 npm install @canonical/styles,或使用 pnpm add / yarn add / bun add。

@canonical/styles 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/canonical__styles 是否存在。

@canonical/styles 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@canonical/styles 可以商用吗?

@canonical/styles 使用 LGPL-3.0 许可。弱 Copyleft:作为库被调用时通常不要求开源你自己的代码,但修改库本身需以 LGPL 开源;前端打包合并后的适用边界存在不同理解。以上为概括,不构成法律意见,请以许可原文为准。

@canonical/styles 的下载量有多少?

根据 npm 官方下载统计,@canonical/styles 在 2026/10/09 – 2026/11/07 这 30 天内被下载 10,367 次,日均约 346 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@canonical/styles 还在维护吗?

最新版本 0.45.0 发布于 2026/10/23(17 天前);至今共发布 57 个版本,其中过去 12 个月发布了 39 个;首次发布于 2024/11/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。