Contributing

How the repo is laid out, how to run it locally, and how to add a component or a customization variable.

Repo layout

The packages and docs live at the repo root. The top-level src/ tree on the master branch is the v2 Bootstrap release and is not actively maintained.

Running locally

Node 24 or newer, and pnpm 10.30.3. The pnpm version is pinned via the packageManager field, so run corepack enable and it matches automatically.

The dev server starts Vite at localhost:5173. CSS rebuilds on save via Tailwind v4’s HMR. The vanilla IIFE bundle is built inline by a Vite plugin and hot-reloads into demo iframes automatically.

To run the token checker across all component files:

This catches undeclared token references, missing --component-* fallbacks, and literal values that should be token refs. Run it before opening a PR.

Adding a component

Each component is one CSS file, one optional JS file, and one docs page. The scaffold command sets up the files and wires the nav entry.

1. Write the CSS

The scaffold creates packages/style/src/<name>/<name>.css. BEM names follow .block, .block__element, .block--modifier. Lowercase, hyphen-separated. Multiple modifiers stack flat on the root and never nest.

Read tokens via fallback-default variables. This keeps the knob overridable from any scope without specificity fights.

Use --spacing(n) for padding, gap, and heights so the component tracks the global density knob. Layout widths and animation anchors can stay as literal rem values.

Derive states with color-mix(in oklch, …) so overriding the base hue automatically repaints hover and active.

Register the component CSS in docs/src/demo/demo.css so the demo iframes pick it up. The scaffold adds the import automatically.

2. Write the behavior (if any)

If the component needs JavaScript, add packages/vanilla/src/components/my-thing.js. Export a class with a constructor that takes a root element, a .destroy() method, and DOM custom events for its lifecycle (stisla:my-thing:opened, stisla:my-thing:closed).

Register it in packages/vanilla/src/index.js so Stisla.init() scans for it.

For state hooks, use [data-state="open"] for open/closed concepts, [data-state="active"] for selected/current, and data-<concept> for Stisla-original states ([data-collapsed], [data-shaking]). No .is-* classes. The CSS reads from the attribute; the JS writes it.

3. Write the docs page

Add docs/src/routes/docs/vanilla/my-thing.tsx. Cover every variant and every state. Rest, hover (a sentence is enough), focus, active, disabled, and invalid where applicable.

Use <Demo> for live previews and <Code> for copyable snippets. One snippet per demo drives both the live render and the shown code.

Page structure: a <header> with <h1> and a short lead, then one <section> per topic. End with a Customization section. The Slider page is the reference shape.

4. Verify

Check the demo page at 320, 768, and 1280 px under both light and dark.

Adding a customization variable

Component-scoped variables open up a knob without touching the spec. Pick a hyphenated name under the block prefix: --button-radius, --slider-thumb-width, --card-padding.

Default to a global token via fallback when the knob is a global concept. Default to a literal when the knob is component-private.

packages/style/src/my-thing/my-thing.css

Document every new knob in the component’s Customization section. One row per variable, with columns for Variable, Default, and Use. The Slider page is the reference shape for how to write that table.

For components with many variables, group the table by purpose (sizing, surface, interaction). For components that mostly rely on shared tokens, a short pointer paragraph back to Styling is enough.

Before opening a PR

  • Run pnpm check from the repo root and confirm it passes clean.
  • Run pnpm build from docs/ and confirm the site builds without error.
  • Open the demo page and walk every state under light and dark at 320, 768, and 1280 px.
  • If you changed a component’s CSS or behavior, run the tests: pnpm test (or pnpm test:rc for Chromium only). See TESTING.md.
  • Run pnpm changeset if you touched a published package (@stisla/style, @stisla/css, @stisla/vanilla), and commit the generated file. Docs-only changes skip this.

One component per PR keeps reviews scoped. A new customization knob can ride along with the component that introduces it.