Scaffold a React + Vite + TypeScript + Tailwind CSS + React Router SPA with optional shadcn/ui, data fetching, and state management. Use this skill when the user asks to "create a new React app", "scaffold a React project", "set up a React SPA", "initialize a React frontend", "new react vite app", or "start a new React project" — even if they just say "new React app".
Scaffold a production-ready React SPA using Vite, TypeScript, Tailwind CSS v4, and React Router. The skill asks minimal questions, applies sensible defaults, and lets the user opt into addons (shadcn/ui, data fetching, state management).
Do NOT use this skill when:
scaffold-nextjs-app instead)create-react-component)setup-react-testing){../../../shared/_progress.partial.md}
{../../../shared/_assets.partial.md}
Ask the user for the project name if not provided. Use it as {project} throughout.
Then ask about optional features. Present defaults clearly — the user should be able to hit Enter and get a working app:
Required questions:
my-apppages/Dashboard.tsx, pages/Settings.tsx instead of generic Home.tsx)Optional addons (default: No for all): 3. shadcn/ui — install and configure shadcn/ui component library? 4. Data fetching — TanStack Query, SWR, or none? 5. State management — Zustand, Redux Toolkit, or none (React state + Context)? 6. Any other libraries — let the user request additional packages
Run the Vite scaffolding command. Always use npm and the react-ts template:
npm create vite@latest {project} -- --template react-ts
cd {project}
Why
react-tsand not a framework template? Thereact-tstemplate gives a clean SPA starting point. Frameworks like React Router (framework mode) or Next.js have their own scaffolding skills.No
npm installyet — dependencies will be installed once, together with Tailwind in the next step.
Install Tailwind CSS as a Vite plugin — the recommended approach for Vite projects. This also installs all scaffolded dependencies from package.json in one pass:
npm install tailwindcss @tailwindcss/vite
Update vite.config.ts to include both React and Tailwind plugins:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": "/src",
},
},
});
Replace src/index.css content with:
@import "tailwindcss";
The @ alias is already set in vite.config.ts (Step 3). Now add TypeScript path resolution.
Add to tsconfig.app.json compilerOptions (merge, don't overwrite) — contents: shared/assets/tsconfig.app.json
{../../../shared/_eslint-prettier.partial.md}
Install React Router for client-side routing:
npm install react-router
Create src/router.tsx:
import { createBrowserRouter, RouterProvider } from "react-router";
import Layout from "@/components/Layout";
import HomePage from "@/pages/HomePage";
import NotFoundPage from "@/pages/NotFoundPage";
const router = createBrowserRouter([
{
element: <Layout />,
children: [
{ path: "/", element: <HomePage /> },
{ path: "*", element: <NotFoundPage /> },
],
},
]);
export default function Router() {
return <RouterProvider router={router} />;
}
If the user described specific app features/modules in Step 1, generate appropriate page routes instead of just
HomePage. For example, if the user said "a task management app", create routes for/,/tasks,/settings.
Create the directory structure:
mkdir -p src/pages src/components src/hooks src/lib
Adaptive structure: If the user described app modules (e.g., "auth", "dashboard", "settings"), create matching page files. Otherwise, use the minimal defaults below.
src/main.tsx — replace the default entry point:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import Router from "@/router";
import "@/index.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<Router />
</StrictMode>,
);
src/components/Layout.tsx:
import { Outlet } from "react-router";
export default function Layout() {
return (
<div className="min-h-screen bg-background text-foreground">
<header className="border-b px-6 py-4">
<h1 className="text-lg font-semibold">{project}</h1>
</header>
<main className="px-6 py-8">
<Outlet />
</main>
</div>
);
}
src/pages/HomePage.tsx:
export default function HomePage() {
return (
<div>
<h2 className="text-2xl font-bold">Welcome</h2>
<p className="mt-2 text-muted-foreground">Your app is ready. Start building.</p>
</div>
);
}
src/pages/NotFoundPage.tsx:
import { Link } from "react-router";
export default function NotFoundPage() {
return (
<div className="flex flex-col items-center justify-center py-20">
<h2 className="text-4xl font-bold">404</h2>
<p className="mt-2 text-muted-foreground">Page not found</p>
<Link to="/" className="mt-4 text-primary underline">
Go home
</Link>
</div>
);
}
Remove default Vite files that are no longer needed:
rm src/App.tsx src/App.css
Apply the optional features the user selected in Step 1.
Initialize shadcn/ui first — this installs dependencies (class-variance-authority, clsx, tailwind-merge), creates components.json, and sets up src/lib/utils.ts automatically:
npx shadcn@latest init
When prompted, select:
The init command detects the Vite + React + TypeScript setup and configures path aliases from
tsconfig.app.jsonautomatically.
Then add commonly used base components:
npx shadcn@latest add button card
Add more shadcn components based on the user's app idea. For example, if the user described a dashboard app, also add
sidebar,dropdown-menu,avatar.
TanStack Query:
npm install @tanstack/react-query
Wrap the app in QueryClientProvider in src/main.tsx:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
// Wrap <Router /> with:
// <QueryClientProvider client={queryClient}>
// <Router />
// </QueryClientProvider>
SWR:
npm install swr
No global provider needed — SWR works per-hook.
Zustand:
npm install zustand
Create src/store/ directory with an example store if relevant to the app idea.
Redux Toolkit:
npm install @reduxjs/toolkit react-redux
Set up src/store/index.ts with configureStore and wrap the app in <Provider>.
Start the development server in async mode so it keeps running:
npm run dev
Then use the Playwright MCP tools to visually verify the app:
http://localhost:5173 using mcp_playwright_browser_navigate.mcp_playwright_browser_snapshot and confirm:
http://localhost:5173/nonexistent using mcp_playwright_browser_navigate, then take another snapshot and confirm the "404" heading and "Go home" link are visible.mcp_playwright_browser_click and take a final snapshot to confirm it redirects back to the home page.If any check fails, diagnose and fix before proceeding.
After scaffolding, show the user what was created:
✅ {project} scaffolded successfully
Stack:
- React 19 + TypeScript
- Vite (dev server + build)
- Tailwind CSS v4 (Vite plugin)
- React Router (client-side routing)
- ESLint + Prettier
{- shadcn/ui (if selected)}
{- TanStack Query / SWR (if selected)}
{- Zustand / Redux Toolkit (if selected)}
Structure:
src/
├── components/ ← shared components
│ └── Layout.tsx
├── hooks/ ← custom hooks
├── lib/ ← utilities
├── pages/ ← route pages
│ ├── HomePage.tsx
│ └── NotFoundPage.tsx
├── router.tsx ← route definitions
├── main.tsx ← entry point
└── index.css ← Tailwind import
Commands:
npm run dev ← start dev server
npm run build ← production build
npm run preview ← preview production build
npm run lint ← run ESLint
Suggest next steps:
create-react-component to add new components"src/router.tsx as your app grows"@tailwindcss/vite — not PostCSS config. Don't create tailwind.config.js or postcss.config.js — they're not needed with the Vite plugin approach.@import "tailwindcss" — Tailwind v4 uses a single CSS import, not the old @tailwind base/components/utilities directives.rsc: false in components.json — this is a Vite SPA, not a server-components app./src not ./src — in vite.config.ts, the alias replacement should be /src (absolute from project root), while tsconfig.app.json uses ./src/* (relative).public/ — Vite serves static assets from public/. Keep it even if empty.eslint.config.js), not .eslintrc. Make sure Prettier integration uses the flat config approach.react-router package (not react-router-dom). The createBrowserRouter API is imported from react-router directly.