Design system
@moteurio/design
The design system behind Moteur’s apps and site: two looks as CSS variables, their fonts, and the motion that goes with them. It has no framework and no runtime dependencies, so anything that can load a stylesheet can use it.
Two looks, two lights each
Drawn from the token files, so all four show at once. The switches beside the page change the whole page.
Discothèque
Lights on
In reviewDiscothèque
Lights off
In reviewFolio
Light
In reviewFolio
Dark
In review
Guides
- Foundations: colour roles, type, spacing, radius, borders, elevation, focus, icons and layout, in both looks.
- Motion and moments: every named motion, its markup, reduced motion and the helper.
- Writing: voice and tone in English and French.
Install
pnpm add @moteurio/design
Three layers
Tokens. Colours, type, spacing, radii, shadows and motion for each look, as CSS variables on
:root. Component CSS reads one set of names whatever the look.
@import '@moteurio/design/looks.css';
@import '@moteurio/design/fonts/discotheque.css';
The same values are available to scripts, and as JSON:
import { looks, motion } from '@moteurio/design';
Moments. Every named motion (a press, a toast, a seal, a drawer unfolding) as CSS keyed on
data-moment, data-state and data-part. Most of them need no script.
@import '@moteurio/design/moments.css';
<button data-moment="press copied" data-state="done">…</button>
Component CSS can use the same arrival, leaving and flash: the moteur-enter, moteur-leave and
moteur-flash keyframes, set with --enter-*, --leave-* and --flash, timed with durations
that reduced motion already caps (--motion-pop, --motion-linger and the rest).
.chip {
--enter-y: 6px;
animation: moteur-enter var(--motion-pop) var(--spring-pop) both;
}
Helper. A small ES module for the few moments CSS can’t do alone: a title flying from a list into the editor, a counter rolling its digits, press and hold, a toast’s timer. It can be tree-shaken.
import { flight, toastTimer } from '@moteurio/design/motion';
Every moment, its markup and the helper’s functions are described in docs/motion.md.
Looks and themes
Discothèque is the default look and needs no attribute. Folio is set with data-look="folio" on
<html>.
Each look has a light and a dark theme. Without an attribute the system setting decides;
data-theme="light" or data-theme="dark" on <html> forces one.
A part of a page can wear Folio whatever the page’s look, such as a preview of one look inside
the other: put data-look="folio" on that element. Its colours, type, shapes and motion tokens
follow inside it, and its theme is the page’s unless it has a data-theme of its own. Moments keep
the page’s look.
Folio’s fonts are separate, so a page loads them only when someone picks Folio:
@moteurio/design/fonts/folio.css.
Reduced motion
Moments follow the system’s reduced-motion setting, and data-motion="reduce" or
data-motion="full" on <html> overrides it. Under reduced motion nothing travels, grows,
bounces or repeats, fades and drawn lines stay short, and timers run as usual. Component CSS can
follow the same rule with --travel, --loop and --motion-cap; see
docs/motion.md.
Linting with stylelint
@moteurio/design/stylelint is a stylelint config that keeps an app’s CSS on the tokens. Install
stylelint (17 or later) and extend it:
// stylelint.config.js
export default { extends: ['@moteurio/design/stylelint'] };
It turns on no style or formatting rules, only these:
- Raw colours: hex, named colours (
transparentandcurrentColorare fine) and colour functions (rgb(),hsl(),oklch()and the rest). Use a colour variable. - Raw motion (
moteur/motion-tokens): a duration or delay written inmsors, or acubic-bezier()orlinear()curve, intransitionandanimationand their longhands.0s,inheritand anything insidevar(…)pass, except an uncapped token as the duration of amoteur-keyframe:moteur-enter var(--spring-pop-duration)should bemoteur-enter var(--motion-pop), which stays short under reduced motion. - Killing motion (
moteur/no-motion-kill): durations above zero and under 10 ms, likeanimation-duration: 0.01ms, and, for reduced motion,animation: none,transition: noneor!importanton an animation or transition. These skip moments and fire timers at once; reduce motion with--travel,--loopand--motion-capinstead (see docs/motion.md). - Keyframes that move (
moteur/keyframes-travel):transform,translate,scaleorrotatein@keyframeswithoutvar(--travel)in acalc(), so the move would still happen under reduced motion. Values that move nothing (none,scale(1)) pass.
An app that keeps colour values in a token file of its own turns the colour rules off there:
export default {
extends: ['@moteurio/design/stylelint'],
overrides: [
{
files: ['src/tokens.css'],
rules: { 'color-no-hex': null, 'color-named': null, 'function-disallowed-list': null },
},
],
};
For a real exception, disable one rule for one line and say why:
/* stylelint-disable-next-line moteur/keyframes-travel -- a timer, not travel */.
Editor help
The package has VS Code custom data, generated from the tokens and the moments: hover a variable
to see what it’s for and its value in each look and theme, and get completions for every
--variable and for the data-moment, data-state, data-part, data-look, data-theme and
data-motion attributes. Add the files to .vscode/settings.json:
{
"css.customData": ["./node_modules/@moteurio/design/vscode/css-custom-data.json"],
"html.customData": ["./node_modules/@moteurio/design/vscode/html-custom-data.json"]
}
Building and checking
The CSS and JS for the looks are generated from tokens/*.json, and so are the contrast tables in
docs/foundations.md and the files in vscode/:
pnpm build # writes css/discotheque.css, css/folio.css, css/looks.css, js/tokens.*, vscode/*
# and the contrast tables
pnpm check # fails if any of those are out of date, if a colour pair is below its contrast,
# or if moments.css breaks the stylelint config
pnpm test # plays every moment in a browser, in both looks, with and without reduced motion
css/moments.css and js/motion.js are written by hand.
Contrast. tokens/contrast.json lists the colour pairs that must stay readable, each with its
minimum: 4.5:1 for text, 3:1 for large text and for focus rings and the edges of controls. The
check measures every pair in both looks and both themes, on the values the generated CSS gives
each variable, and prints a table of the pairs that fall short.
An app can hold its own colours to the same standard: @moteurio/design/contrast exports the
checker’s ratio(fg, bg) (WCAG contrast, hex or rgb()) and shown(ratio) (rounded down, as
the tables show it).
Reduced motion. pnpm test opens test/fixture/, a page with one live example of each
moment, in Chromium through playwright-core, and plays them all. In each look, with reduced
motion and without, it checks that the page logs no errors, that what each moment leaves on screen
ends visible, that the toast lasts its full time and that --travel is 0 only under reduced
motion. If the browser is missing: pnpm exec playwright-core install chromium. To look at the
page yourself: node test/fixture/serve.mjs.
License
MIT