@dume/ds

中文文档
0.10.0发布于 2026/11/12MITTypeScriptES Module构建来源已验证近 30 天下载 2309

My design system: the styles shared by my projects (simon.duhem.fr, Music Bridge), design tokens, base, layouts and components

原始使用文档
文档目录11 节
  1. Naming
  2. Choosing what to add
  3. Web components
  4. Interactive states
  5. Typography
  6. Units
  7. Standards
  8. Print
  9. CSS
  10. Components
  11. Development

@dume/ds

My design system: the styles shared by my projects (simon.duhem.fr, Music Bridge…), its custom properties and classes prefixed with ds-. Only what the projects use: tokens, components and layouts come as they repeat from one project to another.

Every component is HTML and CSS first: native elements and a class, usable in any page without JavaScript. A web component, when one is needed, is that same component in a shadow root, with the behavior it adds (see Web components).

Every interactive element has all its states, and they are accessible: see Interactive states.

While in version 0, it covers my projects' needs only, and a minor version may change what is there: use it elsewhere at your own pace, its version pinned.

Naming

Custom properties are named in lowercase words:

  • --ds-b-{category}-{role}-{variant} for a base token: --ds-b-color-text, --ds-b-color-text-muted, --ds-b-color-background-surface, --ds-b-outline-focus.
  • --ds-c-{component}-{category}-… for a component's (--ds-c-button-size-min-height), --ds-l-{layout}-{category}-… for a layout's (--ds-l-grid-size-gap).
  • --ds- is the namespace; b, c, l the layer.
  • The category, from a closed list: color, font, size, or the CSS property a composite value goes to (outline).
  • The role, then the variant, from the most general to the most specific.
  • The light and dark themes change the values, never the names.

Not --ds-b-muted-color (the category comes first), --ds-b-color-surface (the role, background, is missing), --ds-b-focus-outline (focus is no category: the CSS property is).

@dume/dev-config's Stylelint configuration checks the names (custom-property-pattern) in any project, without depending on this package. Its tests check its tokens (css/tokens.css) the same way. A new category goes in this list, in the Stylelint rule and in the test.

Only the tokens the projects use: a new one comes with its first use.

HTML elements are styled without classes, in the base: links, headings, a heading and its tagline (hgroup), tables, quotes, code. Classes are for components (c-), layouts (l-) and utilities (u-), after the .ds- namespace, in BEM (.ds-l-page--centered), and only where a style needs them: a block's class on its root, an element's on what its block styles, inside it. No utility-first nor framework classes. A modifier only when its block or element is also used without it, or when it is a variant of its own (.ds-c-field__control--multiline: a select or an input is the control without it).

Choosing what to add

Before adding anything, go through these in order, and stop at the first that fits:

  1. A native element does it: style the element in the base, without a class, or use it as it is (<progress>, role="status" already in the page).
  2. A look, for one block: a component, .ds-c-* (a card, a button). Its variants are modifiers.
  3. An arrangement of blocks: a layout, .ds-l-*, or a modifier of one (.ds-l-cluster).
  4. A behavior that the browser does not have, or a page that is not ours: a web component, built on the component of step 2 (see Web components).

A component does not decide what its use does: its place on the page (an alert has no position: the page places it) and its role (status, or alert for what is urgent) are the page's.

What belongs to one project stays in that project, built from these. Say what each step would bring: a web component that would add only JavaScript is not one.

Web components

A component is HTML and CSS first: a native element (<a>, <button>, <dialog>, <details>…) and a class. It becomes a web component only when it needs what CSS and native HTML cannot give:

  • a behavior of its own, in JavaScript, that the browser does not have (Music Bridge's message, closed with its button or Escape);
  • styles isolated from the page in a shadow root, when the page is not ours (Music Bridge's message, on a music platform's page).

Not for a look alone (a button, a field: a class is enough), nor for what a native element already does. Its content works without JavaScript wherever it can, the web component enhancing it.

A web component is built on a component of css/components, which works without it in a page of ours: its shadow root takes that component's stylesheet and classes, and adds only what the page's base would give (links, focus ring) and its own place on the page. <ds-alert> is the alert (.ds-c-alert) in a shadow root.

Web components are named ds- too (<ds-alert>); third-party ones keep their name (<heading-anchors>).

They live in web-components/, in TypeScript, each with its styles in its shadow root: its module imports them (the tokens set on :host, its component's stylesheet, its own), and turns their rem into px, as a page that is not ours has its own root font size.

Web componentModuleWhat it is
Alert@dume/ds/web-components/alert.js<ds-alert>, a message at the top of a page that is not ours, announced by screen readers, closed with its button or Escape: showAlert(document, { text, lang, closeLabel, iconUrl, link }), without the custom elements registry, which an extension's content script does not have
File field@dume/ds/web-components/file-field.js<ds-file-field>, around a file field of the page (.ds-c-field), in its slot: it also takes a file dropped anywhere on it, shows a file over it (dragging), and refuses one its input does not accept, the page's change not fired, saying why in its message (invalid-type-message, too-many-message). Without JavaScript, a file field

Interactive states

Every interactive element (link, button, control) has all its states, each defined by the design system, never left to the browser's defaults:

StateWhat changesSelector
Restthe component's own look
Hoveredthe primary color, darker (light theme) or lighter (dark theme); a tinted background; a thicker underline:hover, unless disabled
Keyboard focusthe focus ring, 3px in the primary color, 2px off the element: on everything, never removed:focus-visible
Presseddarker or lighter still, a more tinted background:active, unless disabled
Disabledgrey text and background, the not-allowed cursor, no hover:disabled, [aria-disabled='true']
Invalid (a control)a 2px border in the danger color, a message saying why[aria-invalid='true'], __message--danger
Current (a navigation link)in bold, underlined 2px[aria-current]

They are accessible:

  • A state never shows by its color alone: a link is always underlined, thicker when hovered; the current page is also in bold; an invalid control has a thicker border and a message, its text saying what is wrong.
  • Contrasts: 4.5:1 for text, 3:1 for borders and the focus ring, in every state, in both themes, on both backgrounds. The tests check them as people see them, and as the three kinds of color blindness do (protanopia, deuteranopia, tritanopia, simulated). WCAG asks nothing of a disabled element: its grey is meant to fade.
  • Disabled with aria-disabled="true" looks the same as with disabled, for a link, or a button that stays in the keyboard order to explain why it cannot be used.
  • Windows' contrast themes (forced-colors): a disabled element in the system's grey text color (GrayText); a ghost button's transparent border shows there.
  • Buttons and links have the pointer cursor, as most design systems do (GOV.UK, USWDS, Carbon, Primer, DSFR), the not-allowed one when disabled.
  • Targets of 24px at least (WCAG 2.5.8): a button, with its padding and line height; the alert's close button, 32px.

The board of states, tests/states.html, shows every interactive element in each of its states at once, from the design system's own stylesheet. A page cannot hover nor press an element itself: the accessibility test forces in Chromium the state each element names in data-state (hover, focus-visible, active), as the DevTools do, so the very :hover, :focus-visible and :active rules apply.

The accessibility test (pnpm --filter @dume/ds test:a11y, in the Accessibility workflow) loads it in both themes, with Playwright Test:

  • axe checks it, and so the contrast of every state, which it cannot do on a page nobody hovers;
  • each state must look other than the element at rest (color, background, border, outline, underline, weight or cursor);
  • the board must match its screenshots, tests/screenshots/light.png and dark.png: a color, a border, a corner or a size that changes fails the test.

The board renders in the Chromium of Playwright's Docker image, the version of @playwright/test: the same fonts and the same rendering on every computer and in CI, so the screenshots match anywhere. Docker is needed. After a change meant to change a state, pnpm --filter @dume/ds test:a11y --update-snapshots takes them again: look at them in the diff. When the test fails, the screenshots taken and their differences are in test-results/, and among the artifacts of the workflow.

The state colors are tokens (--ds-b-color-primary-hover, -active, --ds-b-color-background-hover, -active, --ds-b-color-text-disabled, --ds-b-color-background-disabled, --ds-b-color-danger): a component takes them, never a color of its own. A new component lists its states in the table of components.

Typography

Every heading level has a size and a weight of its own, on the base 4, down to the text's size:

LevelSizeWeightMargins (above, below)
h132pxextra bold0, 16px
h224pxbold40px, 16px
h320pxextra bold32px, 8px
h416pxextra bold24px, 8px
h516pxbold24px, 8px
h616pxsemibold24px, 8px
  • A heading and its tagline go in a hgroup: it takes its heading's margins, and a tagline in small print (a <small> alone in its paragraph, such as a date) sits right under it. A h1 keeps its own margin below.
  • A paragraph is 65 characters wide at most, for reading, whatever the page's width; printed, the sheet's whole width.
  • A heading right after another has no margin above: the previous heading's below sets the room between them (a CV's company and its first position).
  • .ds-l-stack puts the details of an entry one under the other, 8px apart, closer than paragraphs.

The board of typography, tests/typography.html, shows the heading levels, each with its size, weight and margins read from its computed style, a heading and its tagline, the text's elements and the stack. The accessibility test checks it with axe, checks that each heading level looks other than the others and that the h6 is in the text's size, and compares it with its screenshots, tests/screenshots/typography-light.png and typography-dark.png, as the board of states. In Playwright's Docker image, the text's system-ui font is one without a bold face: <strong> shows in the regular weight on the screenshots only, its computed weight being 700.

Units

  • rem for sizes, on the base 4: multiples of 4px, 0.25rem (0.5rem is 8px, 1rem 16px). They follow the font size the visitor chose in the browser.
  • Spacing, font sizes, radii and border widths from the tokens (var(--ds-b-size-16), var(--ds-b-size-border-1) solid), never written in rem or px: the scale stays in one place. Stylelint (@dume/dev-config) checks both rules.
  • em inside a component, for what grows with its text, such as a button's padding.
  • px for borders and the focus ring: 1px stays 1px, and the 3px ring stays visible whatever the zoom.
  • No unit for line heights (1.5), ch for a reading width (65ch), clamp() for a size that follows the screen between a minimum and a maximum, dvh rather than vh, logical properties (margin-inline, padding-block).
  • A page's root font size stays the browser's: no font-size: 62.5%, which would make every rem 10px.
  • The exception: a component shown in a page that is not ours, such as Music Bridge's message on a music page, is in px, as that page's root font size is not ours.

Standards

  • Plain CSS, no preprocessor: native nesting, custom properties, color-mix(), @layer.
  • The target: Baseline widely available, what every major browser has had for 30 months.
  • Light and dark themes: each color is light-dark(light, dark), which follows color-scheme, the system's by default; color-scheme: dark on a page or an element forces a theme.
  • Left-to-right and right-to-left languages alike: logical properties and values (margin-inline-start, text-align: start), which @dume/dev-config's Stylelint configuration requires.
  • Cascade layers: ds.normalize, ds.base, ds.layout, ds.components, ds.utilities, under any project's own CSS, which wins without a specificity fight. Their order is set in tokens.css, the first stylesheet to import.
  • Windows' contrast themes (forced-colors): components draw their edges with borders and outlines, which those themes keep, not with backgrounds alone.
  • Native controls in the primary color (accent-color), balanced headings and paragraphs without an orphan word (text-wrap).

Print

A printed page is in the light theme, its text in Instrument Sans (where the page loads fonts.css: the same on every computer that prints, while on screen the text is in the system's font), the whole width of the sheet, without the site's header, footer and skip link; a heading stays with its content. .ds-u-print-hidden leaves out what only makes sense on screen, such as a download link. simon.duhem.fr prints its CV page as a PDF this way.

CSS

The whole design system is one stylesheet, @dume/ds/styles.css, with its fonts in fonts/ next to it: serve the package's dist/ folder as is, and load it.

<link rel="stylesheet" href="/ds/styles.css" />

simon.duhem.fr does so: its Eleventy configuration copies dist/ as /ds/.

Its parts are also stylesheets of their own, for a project that only takes some of them:

StylesheetWhat
@dume/ds/fonts.cssBricolage Grotesque, the heading font, and Instrument Sans, the printed text's: their Latin subsets, the files in fonts/ next to it
@dume/ds/tokens.cssThe design tokens, as custom properties
@dume/ds/base.cssmodern-normalize, then the HTML elements, without classes: text, headings, hgroup, links, focus ring, small print (small, three quarters of its text's size), groups of fields (fieldset, legend), folded content (details, summary, hovered and pressed as a link), images, tables, quotes, code
@dume/ds/utilities.css.ds-u-visually-hidden: hidden on screen, read by screen readers; .ds-u-print-hidden: not printed; .ds-u-text-muted: text that comes second; .ds-u-text-primary: text in the primary color; .ds-u-text-center: text centered, a paragraph across its container
@dume/ds/extension-page.cssOn an extension's page, after the others: the design system's font, where Chrome sets its own. Outside any layer, not in styles.css
@dume/ds/layout/*.cssOne stylesheet per layout: page.css (.ds-l-page, __main, --fluid, --large, --full-height, --centered, --tinted), cluster.css (.ds-l-cluster, --between, --center, --equal), grid.css (.ds-l-grid), stack.css (.ds-l-stack)
@dume/ds/components/*.cssOne stylesheet per component (see below)
@import '@dume/ds/tokens.css';
@import '@dume/ds/base.css';
@import '@dume/ds/components/button.css';

A project with a color of its own sets --ds-b-color-primary on :root, in both themes: the states and the surface come from it. The tests check the design system's own color: check that one's contrasts too.

The custom properties are set on :root, with color-scheme: light dark, each color in light-dark():

Custom propertyLightDarkFor
--ds-b-color-text#222#fafafaText
--ds-b-color-text-muted#555#bbbSecondary text
--ds-b-color-background#fafafa#222The page
--ds-b-color-background-surfaceprimary 5%, on the backgroundprimary 6%, on the backgroundA box on the page
--ds-b-color-background-tintedprimary 14%, on the backgroundprimary 14%, on the backgroundThe strongest of a page's gradient (.ds-l-page--tinted)
--ds-b-color-border#ccc#555A separator, a decorative border
--ds-b-color-border-control#8a8a8a#8a8a8aThe border of an input (3:1)
--ds-b-color-primary#4b0195#c9a2ffLinks, focus, buttons
--ds-b-color-text-on-primary#fff#222Text on the primary color
--ds-b-color-primary-hoverprimary 80%, on blackprimary 70%, on whiteThe primary color, hovered
--ds-b-color-primary-activeprimary 60%, on blackprimary 40%, on whiteThe primary color, pressed
--ds-b-color-background-hoverprimary 8%, translucentprimary 14%, translucentA background hovered: the primary color, translucent
--ds-b-color-background-activeprimary 16%, translucentprimary 26%, translucentA background pressed
--ds-b-color-text-disabled#767676#949494The text of a disabled element
--ds-b-color-background-disabled#e8e8e8#333The background of a disabled element
--ds-b-color-danger#a30d31#ffabbaAn error, a destructive action: the primary color's chroma, turned to red
Custom propertyValue
--ds-b-font-family, --ds-b-font-family-heading, --ds-b-font-family-monospacesystem-ui (the system's font), Bricolage Grotesque, monospace
--ds-b-size-4, -8, -12, -16, -20, -24, -32, -40, -480.25rem to 3rem, named after their px, for spacing and fonts alike
--ds-b-size-displayclamp(2.5rem, 8vw, 4rem), a hero's heading
--ds-b-size-radius, --ds-b-size-radius-small0.5rem (8px), 0.25rem (4px) for a small block such as a tag
--ds-b-size-border-1, -2, -41px, 2px, 4px: border widths
--ds-b-outline-focus, --ds-b-outline-offset-focus, --ds-b-outline-radius-focus3px solid var(--ds-b-color-primary), 2px, 2px

Text colors pass WCAG AA (4.5:1) on both backgrounds, in both themes, also as the three kinds of color blindness see them: the tests check them. Bricolage Grotesque is for headings only (and a site's name), not for bold text, buttons or labels. styles.css and fonts.css find its files in fonts/, next to them. Without it, headings fall back to the system font.

Components

One stylesheet per component, after the tokens: @import '@dume/ds/components/button.css';. The button first: other components build on it (the alert's close button).

The alert, as <ds-alert> shows it: the page's script closes it, and puts its text in the status region when it shows it after the page has loaded, for screen readers to announce it. It has no position: showAlert fixes it at the top of a page that is not ours, a page of ours places it in its flow.

<div class="ds-c-alert" lang="en">
  <img class="ds-c-alert__icon" src="icon.svg" alt="" />
  <p class="ds-c-alert__text"><span role="status">A message.</span> <a href="…">A link</a></p>
  <button type="button" class="ds-c-button ds-c-button--ghost ds-c-alert__close" aria-label="Close">×</button>
</div>
ComponentClassesFor
Alert.ds-c-alert (--danger), __icon, __text, __closeA message, in the flow of the page, which chooses its place and its role: status, alert for what is urgent, as it interrupts screen readers. Its close button, optional, a ghost button, which the page's script closes it with. An error, --danger: its background and border tinted by the danger color, its text saying what is wrong. <ds-alert>'s markup
Button.ds-c-button, --outline, --ghostA button, or a link that looks like one, filled; outlined for a secondary action or within the text; ghost, neither filled nor outlined, for a discreet action. Hovered, focused, pressed, disabled (disabled, aria-disabled)
Card.ds-c-card, --horizontalA box on the page, to tell a block from what is around it. --horizontal: an image (rounded) beside a few short lines, cut with an ellipsis when too long, such as an album's cover beside a link, then its title and artist
Checkbox.ds-c-checkbox, __control, __label, __descriptionA native checkbox, its label beside it and its description under the label; in a card for a box. Focused, disabled (disabled on its control)
Radio button.ds-c-radio, __control, __label, __descriptionA native radio button, its label beside it and its description, if any, under the label. The radio buttons of a choice in a fieldset, its question in its legend: side by side in a .ds-l-cluster, one under the other in a .ds-l-stack. Focused, disabled (disabled on its control)
Field.ds-c-field, __label, __control (--multiline), __message (--danger)A label, its control across its container, a message under it whose line is kept. Hovered, focused, invalid (aria-invalid, its message --danger), disabled. A file input has its button outlined.
Skip link.ds-c-skip-linkA link to the main content, shown when focused
Tag.ds-c-tagA word or two beside a title, such as "New": small, bold, on the primary color. Not interactive: a link or a button is not a tag
Header.ds-c-header, __brand, __linkA site's header: its name and navigation, the current page's link (aria-current) in bold
Hero.ds-c-hero, --media, __media--media: an image (__media) beside its text, which goes under it when there is no room. A block that catches the eye, such as at the top of a home page: everything in it a little larger, its heading whatever its level. A page's h1 is otherwise in its own size. --ds-c-hero-size-heading and --ds-c-hero-size-text set its heading's and its text's sizes
Footer.ds-c-footerA site's footer
Links.ds-c-linksA few prominent links, under a home page's introduction
Pagination.ds-c-pagination, __item (--next)Links to the previous and next pages of a series, such as articles: not for a form's steps. In a <nav> with its label
Process list.ds-c-process-listThe steps of a process, numbered, such as how a service works: not the progress of a task. An <ol> with .ds-l-grid and role="list"
CodePrism's classesCode examples highlighted by Prism (Okaidia theme, contrast fixed)

Development

Built with Vite+ (vp pack, its settings in vite.config.ts). Each file in css/ declares its own layer (@layer ds.components { … }), their order set once, in tokens.css, which comes first; its imports are bundled into it, in their layer: modern-normalize before the base (@import 'modern-normalize' layer(ds.normalize)), Prism's Okaidia theme before the code component. pnpm build writes dist/: each stylesheet, styles.css (all of them, from css/styles.css), fonts/ (Bricolage Grotesque's Latin subsets, from @fontsource-variable/bricolage-grotesque, which css/fonts.css declares), and web-components/ with their types. Lightning CSS keeps the syntax as it is for the browsers aimed at (light-dark(), nesting). pnpm test (Vitest) checks the tokens' names, contrasts and sizes, and <ds-alert>.

版本与发布记录

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

版本发布日期距今
0.10.0latest2026/11/121 天前
0.9.02026/11/112 天前
0.8.02026/11/085 天前
0.7.02026/11/049 天前
0.6.02026/11/0310 天前
0.5.02026/11/0310 天前

关于 @dume/ds 的常见问题

@dume/ds 是什么?

My design system: the styles shared by my projects (simon.duhem.fr, Music Bridge), design tokens, base, layouts and components

如何安装 @dume/ds?

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

@dume/ds 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@dume/ds 支持 ESM 还是 CommonJS?

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

@dume/ds 可以商用吗?

@dume/ds 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@dume/ds 的下载量有多少?

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

@dume/ds 还在维护吗?

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