Skip to content

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, so nvm use or mise install picks 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 postinstall hook (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:

  1. links every packages/** workspace package so @root/core, @root/dom and each @root/<component> resolve locally;
  2. runs playwright install chromium chromium-headless-shell (skipped when CI is set), because the unit, a11y and e2e suites all drive a real browser;
  3. installs the Husky hooks - pre-commit runs lint and unit tests, pre-push runs 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 of vite.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.