From dominodatalab
Bootstrap or retrofit a Vite + React 18 + TypeScript project to use the Domino design system. Handles version pinning, theme wiring, and common import mistakes.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dominodatalab:domino-ui-bootstrapThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill makes a project use the Domino design system correctly. It works in three contexts:
This skill makes a project use the Domino design system correctly. It works in three contexts:
CLAUDE.md).In all three cases, the skill executes the work end-to-end (runs commands, edits files), but it adapts to what's already there instead of overwriting blindly.
A handful of choices look arbitrary but aren't — they match the rest of the Domino ecosystem and are easy to get wrong:
@dominodatalab/extensions-tools peer-depends on React 18. React 19 fails at install.HashRouter from react-router-dom@5).#, so they never collide with backend paths or the Domino proxy prefix, and inner navigation/linking works without server-side rewrite rules.@dominodatalab/extensions-tools. Storybook code snippets show imports from @domino/base-components — that's a Storybook-internal alias. Rewrite every such import to @dominodatalab/extensions-tools before pasting into a Domino project. This is the single most common mistake.DominoThemeProviderDecorator wraps the whole React tree. Without it, Domino components render unstyled or crash./preview/<appId>/), so asset and API URLs must be document-relative — not root-absolute. Vite's default base: '/' emits /assets/… and bypasses the proxy; user-written fetch('/api/…') does the same. Set base: './' and build API URLs against document.baseURI. See Step 8. Invisible in local dev (pathname is /), surfaces only after deployment.Treat these as invariants the project must satisfy by the end. How you get there depends on what's already in the target directory.
Ask the user, in a single turn, for:
CLAUDE.md, .mcp.json, .claude/settings.local.json) should live. Default to the target path; set this to a parent directory when the target is a subfolder of a larger repo (monorepos, an existing project with apps under apps/, etc.) — those files belong at the repo root, not nested inside the app folder. Ask explicitly; don't assume. If the user doesn't volunteer one, propose the target path and confirm.CLAUDE.md, UI titles, and the hand-off message. Example: Domino Frontend.package.json's name field. npm requires lowercase, no spaces, no leading dot or underscore, URL-safe. Default to the kebab-case-lowercase of the display name (e.g. Domino Frontend → domino-frontend) and confirm. If retrofitting, default to whatever the existing package.json says and confirm.ask_user_input_v0 once you've inspected the directory (Step 2). The options depend on what you find; see Step 2.If the user has already given any of these in the conversation, skip the corresponding question. If they give only one name, treat it as the display name and derive the package name from it — don't ask twice for the same information, just confirm the normalized package name. Throughout the rest of this skill, "project root" means the path from question 2 (equal to the target path unless the user said otherwise) and "app folder" means the target path from question 1.
Before changing anything, find out what's there. This determines which branch of the skill you take.
Look for:
package.json? If so:
react already a dependency? At what major version?vite a devDependency? Some other bundler (webpack, next, parcel, cra)?@dominodatalab/extensions-tools already appear (any version)?src/ directory with main.tsx / main.jsx / index.tsx / index.jsx?.mcp.json, .claude/, or CLAUDE.md? These live at the project root, not the app folder, so check there even when the app folder is a fresh empty subdirectory.Classify the directory into one of these states, then ask the user how to proceed if needed:
| State | What you found | What to do |
|---|---|---|
| Empty / nonexistent | No files, or directory doesn't exist | Go to Step 3 (scaffold from scratch). |
| Vite + React already | package.json lists vite and react | Skip scaffolding. Go to Step 4 (align deps), then continue. |
| React but not Vite | React present, bundler is webpack/CRA/Next/etc. | Stop and ask the user: do they want to migrate to Vite, or keep their bundler and just add the Domino library? The skill is Vite-shaped; if they keep their bundler, the version pinning and theme-provider wiring still apply, but the scaffolding steps don't. |
| Unrelated content | Files exist but no package.json, or a non-React package.json | Ask the user: overwrite (delete and scaffold fresh), bootstrap in place (let Vite prompt), or pick a different path. |
| Already partially Domino-wired | @dominodatalab/extensions-tools already in deps | Treat as a verification/repair job: check each invariant below and only touch what's actually wrong. |
Run node -v and npm -v. Stop if Node is older than 20.
From the target directory:
npm create vite@latest . -- --template react-ts
Do not run npm install immediately afterward. The next step rewrites package.json so the install picks up the right versions in one pass.
If the directory had existing content and the user chose "overwrite", clear it first (rm -rf the contents, not the directory itself).
package.json to the Domino invariantsYou're enforcing constraints, not writing a fixed file. View the current package.json and make sure it satisfies these:
Required dependencies (add or pin to these exact versions):
@dominodatalab/extensions-tools — latest is fine for fresh projects; use a specific version if the user gave one or if one is already pinned in the existing package.json. (See the post-install pin step at the end of this section.)react — 18.2.0react-dom — 18.2.0react-router — 5.3.4react-router-dom — 5.3.4Required devDependencies (the React typings must match React 18):
@types/react — 18.2.0@types/react-dom — 18.2.0@types/react-router — 5.1.20@types/react-router-dom — ^5.3.3Node-version branch — check this BEFORE leaving the scaffold's toolchain alone.
Run node -v. The current npm create vite@latest scaffold writes Vite 9 / TypeScript 6 / ESLint 10 / @types/node 24, all of which require Node ≥ 20.19. The Domino default workspace image ships Node 20.18.3, so the scaffold's defaults will fail npm run build (rolldown native binding error) and TS will error on erasableSyntaxOnly (a 5.6+ flag) and on missing composite: true for tsc -b.
vite — ^5.4.0@vitejs/plugin-react — ^4.3.4typescript — ~5.5.4 (or ~5.6 if you keep erasableSyntaxOnly — but the simpler path is to drop the flag)@types/node — ^20.12.0eslint, @eslint/js, eslint-plugin-react-hooks, eslint-plugin-react-refresh, typescript-eslint, and globals entries from devDependencies. The Vite-9 scaffold's ESLint pins all require Node ≥ 20.19 — easier to drop them than to find a compatible set.erasableSyntaxOnly line from tsconfig.app.json and tsconfig.node.json (TS 5.6+ only)."composite": true to both tsconfig.app.json and tsconfig.node.json (required when build runs tsc -b).eslint.config.js since ESLint was removed.Leave alone:
@vitejs/plugin-react, and any other tooling already in the file — assuming the Node-version branch above didn't tell you to touch them. Whatever versions are there are fine if they work on the user's Node.scripts if the existing ones work; only add dev/build/preview if they're missing.Remove or downgrade:
react or react-dom is at 19+, downgrade to 18.2.0. Same for the @types/*.react-router-dom is at 6+, downgrade to 5.3.4. Warn the user — their existing routes use v6 syntax (<Routes> / element={}) and will need to be rewritten to v5 (<Switch> / component={} or render={}). Don't silently rewrite their routes; tell them what needs to change.After install — pin the resolved @dominodatalab/extensions-tools version.
Once Step 5 has run cleanly and latest was used, resolve the floating tag to a concrete version and rewrite package.json:
npm view @dominodatalab/extensions-tools version
Replace "latest" in dependencies with the exact version string this returns. Skip if a specific version was already pinned (the user explicitly chose one, or you retrofitted). This keeps future installs reproducible — latest drifts.
npm install
Likely failures:
package.json. Don't use --legacy-peer-deps.@dominodatalab/extensions-tools → the user needs to authenticate to a private npm registry. Stop and ask how they normally authenticate. Don't switch to a tarball or alternative source on your own.The Storybook MCP is what lets future Claude sessions look up real component props instead of guessing. Two files need to exist at the project root (from Step 1 — not the app folder if those are different paths) with the right shape; their exact formatting doesn't matter, but the contents do. They must sit in the same directory: if .mcp.json and .claude/settings.local.json get split across folders, Claude Code won't apply the allow-list to the registered MCP and the server stays disabled.
.mcp.json must register a server named storybook that points at the Domino library's live Storybook:
https://main--60c0de3f60dd96003bdcb1a1.chromatic.com/mcphttpIf a .mcp.json already exists at the project root (with other MCP servers, or from a prior bootstrap), merge — don't overwrite. Add the storybook entry alongside whatever's there. If a storybook server is already registered at a different URL, ask the user before changing it. If no .mcp.json exists at the project root, create one there — never create one inside the app folder.
.claude/settings.local.json must:
mcp__storybook namespace.enableAllProjectMcpServers: true).storybook in the enabled servers.If the file already exists at the project root, merge the relevant fields rather than overwriting. Preserve any other permissions or settings already there. If it doesn't exist, create it at the project root (next to .mcp.json), not inside the app folder.
The project's React entry (src/main.tsx for a fresh scaffold, but it might be src/index.tsx or similar in an existing project) needs to satisfy these invariants:
DominoThemeProviderDecorator from @dominodatalab/extensions-tools wraps the entire app tree. It must be an ancestor of every Domino component that gets rendered.HashRouter from react-router-dom (v5) wraps the app inside the theme provider. Not BrowserRouter, not v6.createRoot from react-dom/client (React 18 idiom).StrictMode is fine to keep if Vite added it; not required.For a fresh scaffold, write a clean main.tsx from scratch. For an existing project:
DominoThemeProviderDecorator so it's an ancestor of any Domino components. The usual place is outermost or just inside StrictMode, but the user's existing structure may dictate otherwise — don't blindly reorder providers that have ordering requirements (e.g., Redux usually goes outermost; auth providers often need to be high up).BrowserRouter from v5, swap to HashRouter. If it's any router from v6, you'll need to convert routes too (see Step 4's note) — flag this to the user before doing it.A heads-up worth passing to the user: in a standalone (non-Domino-backend) environment, DominoThemeProviderDecorator will try to fetch user / white-label data and fail silently. The UI still renders with defaults. If they want to suppress those requests, point them at node_modules/@dominodatalab/extensions-tools/README.md for the static useStoreHook prop.
When this app runs inside Domino it's served under a proxy prefix (e.g. https://<host>/preview/<appId>/). Root-absolute URLs bypass the proxy and 404 — both asset tags emitted as /assets/index-*.js and user-written fetch('/api/…') calls. This is invisible in local dev (where pathname is / and root-absolute happens to work) and only surfaces after deployment.
Two things to get right:
base: './' in vite.config.ts (the scaffold does NOT set this)The npm create vite scaffold leaves base at its default /, which emits root-absolute asset URLs (/assets/…) that bypass the proxy. Set it explicitly so emitted assets are document-relative:
export default defineConfig({
base: './',
plugins: [react()],
})
apiBase resolved against document.baseURIBuild API URLs off document.baseURI so they resolve against the proxied path:
const apiBase = new URL('api', document.baseURI).href
fetch(`${apiBase}/projects`)
Don't construct API URLs from window.location.pathname (pathname.replace(/[^/]*$/, '') + 'api' etc.) — those either bypass the proxy or break under deeper routes. Use document.baseURI.
When to apply this:
apiBase.base is missing//. For API calls, scan for fetch('/, axios.get('/, new WebSocket('ws, and any pathname-based URL construction. Surface root-absolute or pathname-based URLs and recommend the document.baseURI form — don't silently rewrite their fetches (some may intentionally target another host).This overlaps with the dominodatalab:app-deployment skill, which covers the full deploy shape (launch script, port binding, build output location). Point the user there for an actual app publish.
Skip this step for retrofits — don't overwrite the user's existing App.tsx and don't touch their existing CSS.
For a fresh scaffold, replace Vite's default App.tsx with something that exercises a few real Domino components, to prove the install works. The minimum it should demonstrate:
@dominodatalab/extensions-tools (so the install path is verified).Button, Card, Typography) — this proves Step 7 is wired correctly.IconResolver to demonstrate the icon system.src/index.css (fresh scaffolds only)Vite's default src/index.css ships ~80 lines of template chrome: a fixed-width centered #root, custom color tokens, h1/h2/p/code overrides, dark-mode rules, and decorative styles for the Vite hero. All of it overrides or conflicts with DominoThemeProviderDecorator's tokens. On a fresh scaffold this isn't "the user's app CSS" — it's template noise.
Overwrite src/index.css with a minimal reset:
body { margin: 0; }
#root { min-height: 100svh; }
Delete src/App.css if App.tsx no longer imports it.
This overrides the "don't remove existing CSS imports" line in Step 7 for the fresh-scaffold case. Step 7's guidance is about preserving real user CSS in a real codebase; it doesn't apply to Vite-template chrome on an empty scaffold. On retrofits, the Step 7 rule stands — leave existing CSS alone.
These are the components and prop shapes confirmed to work against the published @dominodatalab/extensions-tools. Use them for the starter screen without needing to consult the MCP. For anything beyond this set, query the Storybook MCP first (see the CLAUDE.md workflow in Step 10) — don't guess.
| Component | Safe usage |
|---|---|
Button | type='primary' | 'secondary' | 'tertiary', onClick, children. |
Card | title, extra, helpMessage, noPadding, children. No size prop. |
Row / Col / Space | Ant-style. Space takes direction, size. |
Tag | type (not color). Values: user-generated, success, danger, warning. |
Typography | Namespace, not a wrapper. Render Typography.H1 / .H2 / .H3 / .Text. Never <Typography>…</Typography> — it'll throw React error #130 at runtime. |
Typography.Text | Optional type='BodyDefault' | 'BodyDefaultStrong' | 'BodySmall' | 'BodySmallStrong' | 'BodyCode'. |
SpinnerWrapper | Loading wrapper. |
node_modules README uses the Storybook aliasnode_modules/@dominodatalab/extensions-tools/README.md (and any code snippets it embeds) imports from @domino/base-components — that's the Storybook-internal alias, not the published package name. Even though node_modules reads as authoritative, every import in this project must use @dominodatalab/extensions-tools. The CLAUDE.md written in Step 10 reminds future sessions of this, but the starter screen is the first place it can go wrong.
CLAUDE.mdThe project root (from Step 1 — not the app folder if those are different paths) should have a CLAUDE.md that tells future Claude Code sessions four things:
@dominodatalab/extensions-tools. All imports in this project use that name.node_modules/@dominodatalab/extensions-tools/README.md) import from @domino/base-components — that's a Storybook alias. Rewrite to @dominodatalab/extensions-tools before pasting./preview/<appId>/). Two things keep URLs working (see Step 8): base: './' in vite.config.ts, and an apiBase resolved against document.baseURI. Root-absolute URLs (fetch('/api/…')) bypass the proxy and 404.
const apiBase = new URL('api', document.baseURI).href
fetch(`${apiBase}/projects`)
It should also describe the MCP lookup workflow (list-all-documentation → get-documentation → get-documentation-for-story), note that React 18 / react-router 5 versions are pinned for peer-dep reasons, and remind that all backend URLs route through apiBase (not root-absolute paths).
If a CLAUDE.md already exists at the project root, merge rather than overwrite — preserve whatever project-specific guidance is there, and add a Domino section. The user's existing CLAUDE.md may have important info about their codebase that you'd erase by replacing it. If no CLAUDE.md exists at the project root, create one there — never inside the app folder, even when the app folder is a subdirectory of the project root.
.gitignoreThe app folder (the target path from Step 1) needs a .gitignore that excludes the things this skill (and a normal Vite + Domino workflow) generates but that shouldn't be committed. Make sure the following entries are present:
node_modules/ — npm install output.dist/ and build/ — Vite production builds..vite/ — Vite's dev cache.*.log, npm-debug.log*, yarn-debug.log*, yarn-error.log* — package manager logs..DS_Store, Thumbs.db — OS junk..env, .env.local, .env.*.local — local environment files. Keep .env.example if the project has one..claude/settings.local.json — this is the per-user MCP/permissions file. It can leak machine-specific paths and personal preferences. .mcp.json should be committed (it's shared project config); .claude/settings.local.json should not. Only add this entry when the project root equals the app folder — when they differ, .claude/settings.local.json lives at the project root (Step 6), so it belongs in that directory's .gitignore. Don't create or modify a project-root .gitignore for this; surface the missing entry to the user and let them decide.How to handle the file itself:
.gitignore doesn't exist, create it with the entries above.node_modules once by accident, or .claude/settings.local.json is already tracked), don't run git rm on their behalf — surface it and let them decide.The most reliable verification is a clean production build:
npm run build
npm install is not a sufficient check. It can succeed while the toolchain is broken in ways that only surface at build time — most commonly the rolldown native-binding error when Vite 9 runs on Node < 20.19, which install happily writes to disk and only fails when build tries to load. Always run npm run build before reporting success.
This catches version mismatches deterministically. If it passes, the pinning and the entry-point wiring are correct.
npm run dev is a softer check — it'll start even with some misconfigurations. Run it if the user wants to eyeball the result, but npm run build is the one that gates "done".
npm run build does NOT catch the proxy-URL bug from Step 8 — that's a runtime/deploy failure, not a compile one. After a successful build, also confirm the URL wiring by inspecting the built dist/index.html:
src="./assets/…", href="./assets/…") — not /assets/…. If they're root-absolute, base: './' is missing (Step 8a).new URL('api', document.baseURI) (Step 8b). grep -rn "pathname.replace" src/ should return nothing — any hit is a pathname-based URL builder to replace.Expected, not a failure: Vite will emit a warning that some chunks are larger than 500 KB. The Domino library bundles a lot — this is normal for Domino apps and doesn't need chasing.
If build fails:
Cannot find module @rolldown/binding-* or similar) → the Vite toolchain in package.json requires Node ≥ 20.19 but the workspace is on an older Node. Go back to Step 4's Node-version branch and apply the downgrade set.@types/react versions don't match. Recheck Step 4.erasableSyntaxOnly or missing composite → leftover from a TS-6 scaffold on a downgraded TS. Recheck the Node-version branch in Step 4 (strip erasableSyntaxOnly, add composite: true).@dominodatalab/extensions-tools → install didn't complete. Check node_modules/@dominodatalab/extensions-tools/dist.If the build is green but assets or API calls 404 after deploy:
/assets/…, or fetch('/api/…')) bypass the proxy.base: './' (8a) and apiBase via document.baseURI (8b).Tell the user:
CLAUDE.md you wrote will remind the next Claude session about).>500 KB chunk warning on build because the Domino component library bundles a lot. It's not a failure — don't chase it.app.sh launch script that runs npm run build to produce the frontend bundle. Remind them that npm run build alone is not enough to serve the app — they'll need a backend or a static file server to actually serve the built dist/ output. Point them at the dominodatalab:app-deployment skill for the full deploy shape.@dominodatalab/extensions-tools source. Only the published npm package is consumed; upstream changes need a release from the library repo.Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
npx claudepluginhub anthropics/claude-plugins-official --plugin dominodatalab