Getting Started
How to install, run and work on the root design system monorepo on your own machine. For what root is, read the README.
Prerequisites
-
Node.js 24 or newer
(
engines.node: >=24). The repo pins the version in.nvmrc(lts/*) and.tool-versions, sonvm useormise installpicks the right one. -
pnpm 11
- 11.22.0 is what CI runs. npm and yarn are not supported here; the workspace protocol (
workspace:^0.0.1) that links the 57 component packages together is a pnpm feature. -
Chromium, installed for you by the
postinstallhook (see below). Nothing else is needed - no Docker, no database, no environment variables.
There is no corepack step: pnpm is installed once, explicitly, and the same version is pinned in CI.
node --version # v24.x or newer
npm install --global pnpm@11.22.0 # or: brew install pnpm
pnpm --version # 11.22.0
Install
git clone <this-repository> root
cd root
nvm use # or: mise install
pnpm install
pnpm install does three extra things beyond fetching dependencies:
-
links every
packages/**workspace package so@root/core,@root/domand each@root/<component>resolve locally; -
runs
playwright install chromium chromium-headless-shell(skipped whenCIis set), because the unit, a11y and e2e suites all drive a real browser; -
installs the Husky hooks -
pre-commitruns lint and unit tests,pre-pushruns the Playwright e2e and a11y suites.
If pnpm reports ignored build scripts, approve them once with pnpm approve-builds.
Run the docs site
Two ways, and the difference is what the site loads.
pnpm dev # component sources, every component, breakpoints work
pnpm start # the built bundles, only what is Ready for Dev
pnpm dev
generates the site in development flavour and starts the Vite dev server:
src/components.js
imports @root/button and the rest from source
(packages/components/Button/lib/index.js, through the aliases in
vite.config.mjs), so a debugger in a constructor is hit and DevTools shows the
file you are editing. Every component is on the site in this mode, including the ones still in progress -
each marked WIP in the sidebar. Open
http://localhost:1234.
pnpm start
builds and serves dist/ with vite preview: the per-package build output, and
only the components flagged root.readyForDev. That is what deploys, and what the e2e and a11y
suites run against. Open http://localhost:1234.
Both pin port 1234, so run one at a time. The e2e and a11y suites take 4321 instead and build their own
dist/, so pnpm test:e2e runs beside a live pnpm dev without
adopting it - a suite pointed at the dev server measures transformed src/, not what deploys.
root_DOCS_PORT moves the suites again if 4321 is taken.
Ready for Dev
A component is releasable when its own package.json says so:
{ "root": { "readyForDev": true } }
Absent counts as not ready. pnpm release:packages skips anything unflagged, the production
site has no page for it, and dist/ carries no bundle for it. Flag it and it appears
everywhere on the next build, with no other edit - the a11y gate derives its component list from the
same flag.
Either way the site is one page per component, generated from that component's own
README.md, with live examples running the real component code.
The port is pinned with strictPort, so if 1234 is taken the command fails instead of
silently moving to 1235 - the Playwright suites expect that exact origin.
Vite is never used as a bundler here: each custom-element tag is derived from its class name, so a minifier that anonymises named class declarations registers everything as
root-and nothing upgrades. See the comment block at the top ofvite.config.mjs.
What the build actually does
pnpm build runs prebuild first, so one command covers the whole chain:
| Step | Script | Output |
|---|---|---|
| 1 | pnpm lint:tokens |
Fails if a foundation design token is referenced with no fallback and never defined. |
| 2 | pnpm lint:readmes |
Fails if a component README breaks the template or documents an event the code never dispatches. |
| 3 | pnpm build:packages |
Per-package build (scripts/_build-package.mjs): <name>.min.css, and
<name>.min.js with that stylesheet inlined as a string.
root_EMBED_STYLES=0 leaves it out.
|
| 4 | pnpm docs:generate |
Renders every component README into the static src/index.html. |
| 5 | pnpm build |
scripts/_docs-dist.mjs
assembles dist/ by copying src/ plus the real per-package output.
|
The component bundles are never re-bundled or minified by Vite. Each custom-element tag
is derived from its class name, so a minifier that anonymises named classes would register every component
as root- and nothing would upgrade.
dist/
is exactly what gets deployed, which is why every Playwright suite runs against it.
Working on a component
Components live in packages/components/<Name>/. Editing source alone changes nothing on
the page - the docs load the compiled <name>.min.js - so keep a watcher running:
pnpm watch:js # esbuild --watch for every package's JS
pnpm watch:css # esbuild --watch for every package's CSS
Then reload the docs page. To rebuild a single package instead of all of them:
pnpm --filter @root/button run build
Documentation is part of the component: packages/components/<Name>/README.md
is the docs page. After editing it, run
pnpm docs:generate
(or pnpm build) to regenerate src/index.html.
Running the tests
pnpm test # Vitest in a real Chromium - 546 unit tests across 69 files
pnpm test:watch # the same, in watch mode
pnpm test:coverage # with the V8 coverage report
pnpm test:e2e # Playwright: the e2e suite and the per-component axe WCAG 2.2 AA scan
pnpm test:e2e
runs 40 checks and starts its own server (pnpm build && vite preview), so allow it
the first-run build time. An already running pnpm start is reused outside CI.
To run one component's unit tests:
pnpm --filter @root/button run test
Adding a new component
pnpm component:add --component-name="colorPicker"
The scaffolder copies scripts/templates/component, renames the placeholders, aligns the
version and workspace dependencies, optionally adds the package to the root package.json and
runs pnpm install. Use --dry-run first to see what it would touch. Afterwards,
write the README against
docs/component-readme-template.md
- pnpm lint:readmes will hold you to it.
Committing
Conventional Commits, enforced by commitlint. The scope must be a component id or one of the shared scopes (core,
dom, foundations, docs, toolchain,
styleguide, testing, form-controls) - the full list is in
commitlint.config.js.
git commit -m "feat(button): add the loading state"
Remember the hooks: pre-commit runs pnpm lint and pnpm test,
pre-push runs pnpm test:e2e.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
Port 1234 is already in use |
Another vite preview or Playwright web server is running. Stop it, or free the port -
the port is intentionally not negotiable.
|
| A component renders unstyled or stays an inert plain element |
Its <name>.min.js is missing or stale - the styles are inside it. Run
pnpm build:packages, or keep pnpm watch:js running. A console warning
about root_EMBED_STYLES=0 means the bundle was built without them on purpose.
|
| The docs page shows an old README | src/index.html is generated. Run pnpm docs:generate. |
| Playwright errors about a missing browser |
pnpx playwright install chromium
(the postinstall hook is skipped when CI is set).
|
| The build fails on an undefined token |
pnpm lint:tokens
caught a var(--root-x--…) with no definition and no fallback. Define the token in
packages/css/foundations/lib/ or add a fallback.
|
| Anything stale after a dependency change |
pnpm run clear:cache
(drops node_modules/.vite and dist/), or pnpm run reset for a
full reinstall.
|
Command reference
| Command | What it does |
|---|---|
pnpm dev |
Serve the docs from the component sources - breakpoints work, drafts included. |
pnpm start |
Build, then serve the deployable site (Ready for Dev only) on http://localhost:1234. |
pnpm build |
Lint tokens and READMEs, build every package, generate the docs, assemble dist/. |
pnpm build:packages |
Build every package's CSS and JS with esbuild, in parallel. |
pnpm docs:generate |
Regenerate src/index.html from the component READMEs. |
pnpm watch:js / pnpm watch:css |
Rebuild every package on change. |
pnpm lint / pnpm lint:fix |
Biome lint, with or without writing fixes. |
pnpm lint:tokens / pnpm lint:readmes |
The two custom gates that run before every build. |
pnpm test / pnpm test:watch / pnpm test:coverage |
Unit tests. |
pnpm test:e2e |
Playwright e2e plus the per-component WCAG 2.2 AA axe scan. |
pnpm component:add |
Scaffold a new component package. |
pnpm run clear:cache / pnpm run reset |
Clear caches; wipe and reinstall everything. |