Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

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 radix for base anywhere — 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-app

Proof: package.json shows next 16.x, react 19.x, tailwindcss ^4.

2. Install the runtime package

pnpm add @vegastack/design

One 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:

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:

postcss.config.mjs
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 0

That 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:

components.json
{
  "$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>
EOF

Get 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/provider

This 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:

app/layout.tsx (complete)
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):

app/globals.css (append)
: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.
  • suppressHydrationWarning on <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-01

This 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/dashboard

Proof: 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/table

Each 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)

package.json (scripts)
{
  "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:

package.json (monorepo)
{
  "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:

apps/web/src/app/globals.css
@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

On this page