Quickstart
Zero to a working VegaStack dashboard in one sitting — every command, every file, no surprises. Mirrors the reference starter step for step.
Last updated
This is the complete path from an empty directory to a themed, toast-firing,
dark-mode dashboard built from copy-in components. Every command below was executed
verbatim to build the reference starter (vegastack-design-starter); if a step here
disagrees with reality, that's a bug — report it.
You need: Node ≥ 20 (≥ 24 recommended), pnpm, the registry service token (see Registry access & auth), and ~10 minutes.
For agents: every step is idempotent and non-interactive unless marked. Execute in order; each step names the file it creates or changes and the proof it worked. Do not substitute
radixforbaseanywhere — VegaStack components are Base UI.
1. Scaffold (Next 16 + Tailwind v4 + pnpm)
pnpm create next-app@latest my-app --ts --app --tailwind --eslint \
--import-alias "@/*" --use-pnpm --yes
cd my-appProof: package.json shows next 16.x, react 19.x, tailwindcss ^4.
2. Install the runtime package
pnpm add @vegastack/designOne package — @vegastack/design-tokens (the zero-dependency token layer) arrives
as its dependency. Proof: ls node_modules/@vegastack shows design. Under pnpm,
design-tokens resolves transitively at this point — you won't see it top-level yet;
later, shadcn add will list it as a direct dependency of your project (many registry
items declare it). Both states are normal; the packages share a Changesets linked
group (same version whenever both change in a release).
3. One-line CSS setup
Replace the entire contents of app/globals.css:
/* preset.css bundles: tailwindcss + tw-animate-css + the token theme (:root/.dark)
* + the base a11y layer (focus rings, reduced-motion, portal isolation)
* + the utility helpers (shimmer / scroll-fade / scrollbar).
* Do NOT also import "tailwindcss" here — the preset already does. */
@import "@vegastack/design/preset.css";Requires the Tailwind PostCSS plugin. Step 1's --tailwind flag writes
postcss.config.mjs for you. If you scaffolded by hand, are adding VegaStack to an
existing app, or are in a monorepo, create it now — without it Tailwind's engine
never runs:
const config = { plugins: { "@tailwindcss/postcss": {} } };
export default config;Proof: pnpm build succeeds and emits utility classes, not just variables:
grep -o '\.[a-z][a-z0-9-]*{' .next/static/**/*.css | wc -l # expect a real count, not 0That second half matters. The token theme is literal CSS and lands either way, so a
build with no PostCSS plugin can look almost right while every utility silently does
nothing. If the count is 0, or the build fails with Can't resolve 'tw-animate-css'
on a current @vegastack/design, the plugin is missing — see
Troubleshooting.
4. Consumer config: components.json
The shadcn CLI reads this to know where components go and where the private registry lives. Create it at the project root:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-vega",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@vegastack": {
"url": "https://design.vegastack.com/r/{name}.json",
"headers": {
"CF-Access-Client-Id": "${CF_ACCESS_CLIENT_ID}",
"CF-Access-Client-Secret": "${CF_ACCESS_CLIENT_SECRET}"
}
}
}
}The ${…} placeholders are expanded by the shadcn CLI itself (from .env.local /
.env / the shell) — next step.
Proof: node -e "JSON.parse(require('fs').readFileSync('components.json','utf8'))"
exits silently (valid JSON). A typo here otherwise surfaces only at step 6 as an
unrelated-looking fetch error.
5. Registry credentials (never in git)
cat > .env.local <<'EOF'
CF_ACCESS_CLIENT_ID=<your-id>.access
CF_ACCESS_CLIENT_SECRET=<your-long-hex-secret>
EOFGet the values per Registry access & auth. Commit a
placeholder .env.example — note create-next-app's default .gitignore contains
.env*, which ignores the example file too; add an un-ignore line once:
printf '!.env.example\n' >> .gitignore.env.local stays gitignored either way.
Proof (the CLI reads .env.local itself, but your shell doesn't — source it
for this one check):
set -a; . ./.env.local; set +a
curl -s -o /dev/null -w "%{http_code}" \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
https://design.vegastack.com/r/registry.json
# → 200 (a 403 means wrong/missing token values)6. The provider — before any component
pnpm dlx shadcn@latest add @vegastack/providerThis copies components/ui/provider.tsx (+ its toast dependency) into
your repo. Now edit your existing app/layout.tsx — keep the globals.css
import and the font setup the scaffold gave you; the change is the provider import,
the wrapper, and suppressHydrationWarning. The complete edited file:
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; // ← KEEP THIS — removing it silently un-styles the whole app
import { VegaStackProvider } from "@/components/ui/provider";
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { title: "My App" };
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html
lang="en"
suppressHydrationWarning
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<body>
<VegaStackProvider>{children}</VegaStackProvider>
</body>
</html>
);
}And point the token font families at those loaded fonts (append to app/globals.css):
:root {
--font-family-sans:
var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--font-family-mono: var(--font-geist-mono), ui-monospace, monospace;
}Three non-negotiables, all silent failures if skipped:
- Keep
import './globals.css'— dropping it produces a successful build with zero CSS emitted. No error anywhere; the app just renders unstyled. suppressHydrationWarningon<html>— next-themes mutates it client-side.- Provider before components — without it, every
toast()call does nothing, dark mode never applies, and tooltips lose shared-delay coordination. See Provider setup.
7. A real page in one command — the dashboard block
pnpm dlx shadcn@latest add @vegastack/dashboard-01This is a registry:block: it copies a complete dashboard
(app/dashboard/page.tsx + loading.tsx + data.json + four page components)
plus its full component dependency graph — expect ~30 files (AppShell, Sidebar,
Card, Chart, Badge, hooks, …) in one shot. Unlike components, a block is a
starter you own from day one — edit the page freely; it is not update-tracked.
(The reference starter's first adaptation was moving the shell into a shared
app/dashboard/layout.tsx so child routes keep the sidebar — copy that pattern.)
pnpm dev
# open http://localhost:3000/dashboardProof: a sidebar-navigated dashboard with stat cards, a usage chart, and an activity feed — all themed, all dark-mode-ready.
8. Add individual components as you build
pnpm dlx shadcn@latest add @vegastack/dialog @vegastack/field @vegastack/tableEach lands in components/ui/, brings its own registryDependencies
transitively, and is tracked for updates by content (no in-file marker needed).
Seeing ℹ Skipped N files is normal — those dependencies were already installed
by an earlier add.
For the verify-before-copy flow and the update loop
(check-updates → --diff → --overwrite), see
Working with components.
9. Wire the drift gate (CI)
{
"check-updates": "vegastack-design check-updates",
"ci": "pnpm typecheck && pnpm build && pnpm check-updates --fail-on-update"
}vegastack-design is the CLI bin shipped inside @vegastack/design — zero extra
installs. --fail-on-update turns component drift into a red build instead of a
surprise.
In a monorepo, pass --dir. The default resolution tries aliases.ui with a
leading @/ stripped, plus a src/ variant — neither finds components living in a
workspace package, and a gate that scans nothing passes forever. --fail-on-update
exits 1 rather than 0 when it finds zero components, so this misconfiguration is
loud, but the fix belongs in the script:
{
"check-updates": "vegastack-design check-updates --dir packages/ui/src/components/ui",
"doctor": "vegastack-design doctor"
}
``` Details in [Production checklist](/docs/guides/production-checklist).
## Complete file inventory after this guide
| File | Origin | Yours to edit? |
| --------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `app/globals.css` | you (step 3) | yes |
| `postcss.config.mjs` | `create-next-app --tailwind` (step 1), or you | rarely — but required (step 3) |
| `components.json` | you (step 4) | yes |
| `.env.local` | you (step 5) | yes — never commit |
| `components/ui/*.tsx` | copied by `shadcn add` | yes — you own them; updates are pulled, never pushed |
| `app/dashboard/*` | the `dashboard-01` block | yes — a starter, not update-tracked |
| `lib/utils.ts` | optional — create it as `export { cn } from '@vegastack/design';` if any copied file imports `@/lib/utils` | yes |
Anything unclear or broken on this path → [Troubleshooting](/docs/guides/troubleshooting).
## In a monorepo
Everything above assumes a single app. In a pnpm workspace — where components live
in a shared package and the app consumes them — four things change. Each was hit by
a real consumer on the first day.
**1. `components.json` goes in the package too, not only at the root.** The shadcn
CLI detects the workspace and refuses to run without one in the package it is
writing to:
```text
Could not load the workspace config in packages/ui.
Add components.json to this workspace and configure its path aliases.Give that package its own components.json (same registries block) and a tsconfig
mapping "@/*": ["./src/*"], so @/components/ui/* and @/lib/utils resolve
inside the package that owns the files.
2. Tell Tailwind where the components are. Source detection is relative to the CSS file, so anything outside the app's own tree is invisible — copied components in a shared package compile to nothing, silently. Add explicit sources next to the preset import:
@import "@vegastack/design/preset.css";
@source "../../src";
@source "../../../../packages/ui/src";3. Keep tsconfig at strict. Registry components are authored against
strict alone. Stricter flags will fail on code you are not allowed to edit —
exactOptionalPropertyTypes in particular rejects the optional-prop pass-through
every wrapper component uses, and a copied
component must never be patched to satisfy a consumer's compiler settings (an edit
reads as permanent drift against check-updates). Put the stricter flags on the
packages you author instead.
4. Run shadcn add from the workspace root, where .env.local lives — the CLI
expands ${CF_ACCESS_CLIENT_ID} from the directory it is invoked in.
Then confirm the whole setup in one command — it understands workspace layouts and reports each check with its fix:
pnpm exec vegastack-design doctor