shadcn-impl-framework-integration
Per-framework setup matrix for the shadcn ui CLI. Five supported templates
(next, vite, react-router, astro, start) differ in: init flag,
components.json.rsc default, alias-resolution system, Tailwind plugin,
and dark-mode strategy. Each axis is non-substitutable. Pick by framework
first, then apply the exact recipe.
Quick Reference Decision Tree
Q1. Which framework is the host project?
Next.js (App or Pages Router) --> Section: Next.js
Vite + React --> Section: Vite
React Router v7 (former Remix) --> Section: React Router
Astro + React integration --> Section: Astro
TanStack Start --> Section: TanStack Start
Laravel + Inertia --> Run `laravel new` first, then
`npx shadcn@latest init` (no `-t`)
other (CRA, Parcel, Rspack) --> NOT supported by CLI; use Vite recipe
and adapt alias config manually
Q2. Do I run rsc:true or rsc:false?
framework has React Server Components (Next.js App Router, TanStack Start
with RSC enabled) --> rsc:true
framework is client-only (Vite, React Router v7 by default, Astro,
Next.js Pages Router) --> rsc:false
rule: NEVER set rsc:true unless the bundler actually compiles
"use client" boundaries. Wrong value silently breaks SSR.
Q3. Which Tailwind version?
Tailwind v4 (default for Vite, Next.js, Astro, TanStack Start in 2026)
--> single CSS file with `@import "tailwindcss";`, NO tailwind.config.js,
plugin: @tailwindcss/vite (Vite/TanStack Start/Astro) OR
@tailwindcss/postcss (Next.js).
Tailwind v3 (legacy projects only)
--> tailwind.config.{js,ts} with `content` array, HSL space-separated
CSS variables. See shadcn-errors-tailwind-v3-v4-migration.
Per-Framework Setup Matrix
| Framework | Init flag | rsc |
tsx |
Alias system | Tailwind plugin | Dark-mode strategy |
|---|---|---|---|---|---|---|
| Next.js (App Router) | -t next |
true |
true |
tsconfig.json#paths (@/*) |
@tailwindcss/postcss |
next-themes ThemeProvider in app/layout.tsx |
| Next.js (Pages Router) | -t next |
false |
true |
tsconfig.json#paths (@/*) |
@tailwindcss/postcss |
next-themes ThemeProvider in _app.tsx |
| Vite + React | -t vite |
false |
true |
tsconfig.json + tsconfig.app.json + vite.config.ts#resolve.alias |
@tailwindcss/vite |
Custom React Context ThemeProvider |
| React Router v7 | -t react-router |
false |
true |
tsconfig paths (~/*) |
@tailwindcss/vite |
Custom Context in app/root.tsx |
| Astro + React | -t astro |
false |
true |
tsconfig.json#paths (@/*) |
@tailwindcss/vite |
Inline <script is:inline> in Layout |
| TanStack Start | -t start |
true |
true |
tsconfig paths OR package.json#imports |
@tailwindcss/vite |
Custom Context in __root.tsx |
ALWAYS use the matching -t flag. NEVER hand-edit components.json.rsc
to true in a Vite project. NEVER omit the alias entry from
vite.config.ts (TypeScript resolves but Vite's bundler will not).
Section: Next.js
ALWAYS pass -t next. App Router is the documented default; Pages Router
still works but loses the auto-injected "use client" benefit.
# new project
pnpm create next-app@latest my-app
cd my-app
pnpm dlx shadcn@latest init -t next
# add components
pnpm dlx shadcn@latest add button
components.json for Next.js App Router:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"utils": "@/lib/utils",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
tsconfig.json without --src-dir:
{ "compilerOptions": { "paths": { "@/*": ["./*"] } } }
tsconfig.json with --src-dir:
{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
ThemeProvider in app/layout.tsx:
// components/theme-provider.tsx
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider(props: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props} />
}
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
</body>
</html>
)
}
ALWAYS set suppressHydrationWarning on <html>. Without it, next-themes
will log a hydration mismatch on every page load because the server renders
without a theme class and the client adds one before React hydrates.
For Pages Router, mount the same ThemeProvider in pages/_app.tsx and
set components.json.rsc to false (Pages Router has no RSC).
Section: Vite
pnpm create vite@latest my-app --template react-ts
cd my-app
pnpm add tailwindcss @tailwindcss/vite
pnpm add -D @types/node
pnpm dlx shadcn@latest init -t vite
pnpm dlx shadcn@latest add button
vite.config.ts MUST register @tailwindcss/vite AND the @ alias:
import path from "path"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
})
Vite's TypeScript template ships TWO tsconfig files. Both MUST declare the alias or the editor and the bundler will disagree:
// tsconfig.json
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }
// tsconfig.app.json
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }
src/index.css (Tailwind v4 single-line import):
@import "tailwindcss";
components.json for Vite: "rsc": false. ALWAYS. Vite has no concept of
server components and the CLI will skip the "use client" directive if
rsc is true, producing components that fail at runtime when they touch
browser-only APIs.
Vite has NO next-themes equivalent. Use the custom React Context
ThemeProvider in examples.md and mount it at the root of App.tsx.
Section: React Router v7 (former Remix)
pnpm create react-router@latest my-app
cd my-app
pnpm dlx shadcn@latest init -t react-router
pnpm dlx shadcn@latest add button
create-react-router pre-configures Tailwind and the ~/* alias. The
shadcn CLI writes components.json with rsc:false and aliases pointing
at ~/components/ui, ~/lib/utils etc. Imports use ~, NOT @:
import { Button } from "~/components/ui/button"
Loaders and actions are React Router primitives, not shadcn primitives.
shadcn <Form> works with useFetcher or <Form method="post"> from
react-router. See examples.md for the loader-driven pattern.
Dark mode: same custom React Context as Vite, mounted in app/root.tsx.
The <Meta />, <Links />, and <Scripts /> from react-router stay
intact; wrap <Outlet />.
Section: Astro
Astro is islands-first. Static HTML is shipped by default; React
components hydrate only when given a client:* directive.
pnpm create astro@latest astro-app -- --template with-tailwindcss --install --add react --git
cd astro-app
pnpm dlx shadcn@latest init -t astro
pnpm dlx shadcn@latest add button
astro.config.mjs:
import { defineConfig } from "astro/config"
import react from "@astrojs/react"
import tailwindcss from "@tailwindcss/vite"
export default defineConfig({
integrations: [react()],
vite: { plugins: [tailwindcss()] },
})
tsconfig.json:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }
components.json has rsc:false. Astro components (.astro files) are
server-only by definition; shadcn components are React and need a hydration
directive when interactive:
---
import { Button } from "@/components/ui/button"
import ModeToggle from "@/components/mode-toggle"
---
<!-- presentational, no JS needed -->
<Button>Static</Button>
<!-- interactive, MUST hydrate -->
<ModeToggle client:load />
client:load hydrates immediately. client:idle waits for requestIdleCallback.
client:visible waits for intersection. client:only="react" skips SSR.
ALWAYS pick the latest valid directive; never ship interactivity without one.
Dark mode in Astro: inline <script is:inline> in the Layout (runs before
hydration to prevent flash). See examples.md.
Section: TanStack Start
pnpm dlx shadcn@latest init --template start
# or for an existing project:
pnpm dlx shadcn@latest init -t start
pnpm dlx shadcn@latest add card
TanStack Start supports RSC (newer feature). components.json ships with
rsc:true when generated by the -t start template. The bundler is Vite
under the hood; alias resolution can use either tsconfig paths or
package.json#imports. Default is tsconfig paths with @/*.
Components import inside route files:
import { createFileRoute } from "@tanstack/react-router"
import { Card, CardHeader, CardTitle } from "@/components/ui/card"
export const Route = createFileRoute("/")({ component: App })
function App() { return <Card><CardHeader><CardTitle>Hi</CardTitle></CardHeader></Card> }
Dark mode: TanStack Start does NOT ship next-themes. Use the same custom
Context pattern as Vite, mounted in app/routes/__root.tsx around <Outlet />.
Alias Setup Per Framework
ALWAYS keep tsconfig paths and the bundler alias in lockstep. A mismatch produces "module not found" at runtime even when the editor resolves it.
| Framework | Editor (tsconfig) | Bundler | Astro/runtime |
|---|---|---|---|
| Next.js | paths: "@/*": ["./*"] or ["./src/*"] |
webpack/turbopack auto | n/a |
| Vite | paths in tsconfig.json AND tsconfig.app.json |
vite.config.ts#resolve.alias |
n/a |
| React Router | paths: "~/*": ["./app/*"] (auto-generated) |
Vite under the hood | n/a |
| Astro | paths: "@/*": ["./src/*"] |
Vite under the hood | astro.config.mjs integrations only |
| TanStack Start | paths: "@/*": ["./src/*"] OR package.json#imports |
Vite | n/a |
If components.json.aliases.ui = "@/components/ui" then tsconfig MUST
resolve @/components/ui/button to ./src/components/ui/button.tsx
(or ./components/ui/button.tsx without --src-dir). The bundler MUST
agree. NEVER let them drift.
ThemeProvider Per Framework
Three distinct strategies. Mixing them is a recipe for hydration warnings.
- Next.js :
next-themeswith<ThemeProvider attribute="class">andsuppressHydrationWarningon<html>. App Router places it inapp/layout.tsx; Pages Router inpages/_app.tsx. Pages Router users also need a<Script>in_document.tsxto set the class before hydration, OR accept a flash. - Vite, React Router, TanStack Start : custom
ThemeProviderusing React Context + localStorage +matchMedia. No external library. Full code inexamples.md. - Astro : inline
<script is:inline>in the layout, runs before any React hydration, setsdarkclass ondocumentElement. The ReactModeToggleisland usesclient:loadand auseEffectthat re-syncs the class. This is the only pattern that survives Astro's SSR-first model without a flash.
ALWAYS pick the strategy that matches the framework template flag. NEVER
install next-themes in a Vite or Astro project; it depends on Next.js
runtime APIs that are absent.
Companion Skills
shadcn-core-cli: fullinit,add,diff,buildflag referenceshadcn-impl-component-install: per-component add workflow, dry-run, overwrite policy, monorepo-cflagshadcn-impl-rsc-vs-client-boundaries: when each shadcn component needs"use client", validator agent, RSC-safe component listshadcn-impl-theming-custom: token customization, oklch vs HSL,@theme inline, per-framework token wiringshadcn-errors-tailwind-v3-v4-migration: migrating fromtailwind.config.js+ HSL to@import "tailwindcss"+ oklchshadcn-errors-cli-sync-mismatch: recovering frominitoverwriting customizedcomponents.jsonorglobals.css
Reference Links
- https://ui.shadcn.com/docs/installation
- https://ui.shadcn.com/docs/installation/next
- https://ui.shadcn.com/docs/installation/vite
- https://ui.shadcn.com/docs/installation/react-router
- https://ui.shadcn.com/docs/installation/astro
- https://ui.shadcn.com/docs/installation/tanstack
- https://ui.shadcn.com/docs/components-json
- https://ui.shadcn.com/docs/dark-mode/next
- https://ui.shadcn.com/docs/dark-mode/vite
- https://ui.shadcn.com/docs/dark-mode/astro
See methods.md for the init-command + tsconfig matrix per framework.
See examples.md for end-to-end setups per framework.
See anti-patterns.md for the five recurring init failures.