# shadcn Vite Setup with Tailwind CSS v4 and React 19

> shadcn Vite setup tested on Vite 8 and TypeScript 6: the Tailwind v4 plugin, the @ alias in tsconfig.app.json, dark mode without Next.js, then add components.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 13, 2026
- Category: [Next.js and Tailwind CSS](https://wingo-ui.com/blog/category/nextjs-tailwind)
- Reading time: 7 min
- Canonical: https://wingo-ui.com/blog/shadcn-vite-setup

## TL;DR

Create the app with npm create vite@latest -- --template react-ts, add tailwindcss and @tailwindcss/vite, set the @ alias in vite.config.ts and as paths in both tsconfig.json and tsconfig.app.json (no baseUrl on TypeScript 6), then run npx wingo-ui@latest init and add components. Dark mode needs a .dark class on html: a short script in index.html plus the next-themes provider, which runs fine without Next.js. In a project already set up with the shadcn CLI, give the second library its own folder so the two button.tsx files never collide.

Most shadcn Vite tutorials were written for Vite 5 and TypeScript 5, and two of their steps now break. The current Vite template ships TypeScript 6, which rejects `baseUrl`, and it type checks your code through `tsconfig.app.json`, so an alias added only to `tsconfig.json` never reaches it. This guide sets up a Vite, React 19 and Tailwind CSS v4 project that takes shadcn-style components, then installs them with the Wingo UI CLI or the shadcn CLI, with the trade-offs of each.

Everything below was run on October 13, 2026 with `create-vite` 9.2.1 (Vite 8.3.4, React 19.3.0, TypeScript 6.0.3), Tailwind CSS 4.3.3, the `wingo-ui` CLI 1.0.3, the `shadcn` CLI 4.21.4 and next-themes 0.4.6. For the Next.js version of this setup, read the [Next.js component library setup guide](https://wingo-ui.com/blog/next-js-component-library-setup).

## What does a Vite project need for shadcn-style components?

Source-owned components are plain `.tsx` files that import each other through an alias, style themselves with Tailwind utilities and read colors from CSS variables. A Vite Tailwind v4 React project needs five things for them:

| Piece | Where | Why |
| --- | --- | --- |
| React 19 | `package.json` | `ref` as a prop, no `forwardRef` |
| Tailwind CSS v4 | `@tailwindcss/vite` plugin, `@import "tailwindcss";` | the utilities and `@theme` |
| `@` alias for Vite | `vite.config.ts` | so the bundler resolves `@/components/ui/button` |
| `@` alias for TypeScript | `tsconfig.json` and `tsconfig.app.json` | so the type checker and editor resolve it |
| Tokens and `cn` | `src/index.css`, `src/lib/utils.ts` | colors, radius, motion, class merging |

## How do I do a shadcn Vite setup step by step?

### 1. Create the project and install Tailwind

```bash
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install tailwindcss @tailwindcss/vite
npm install -D @types/node
```

The template already lists `@types/node` as of create-vite 9.2.1. Installing it again is harmless, and older templates need it for `path` in the config.

Replace everything in `src/index.css` with one line, and delete `src/App.css` along with its import in `src/App.tsx`. The starter CSS styles `#root`, `h1` and `body` and would fight the components.

```css
@import "tailwindcss";
```

### 2. Add the plugin and the Vite path alias

```ts
// vite.config.ts
import path from "node: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") } },
})
```

This Vite path alias is for the bundler only. TypeScript does not read `vite.config.ts`, so it needs its own copy.

### 3. Set tsconfig.app.json paths, without baseUrl

The template's `tsconfig.json` has `"files": []` and two references. Your `src` is checked through `tsconfig.app.json`. Add the alias to both:

```json
// tsconfig.json
{
  "files": [],
  "compilerOptions": { "paths": { "@/*": ["./src/*"] } },
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}
```

```json
// tsconfig.app.json, inside compilerOptions
"types": ["vite/client", "node"],
"paths": { "@/*": ["./src/*"] },
```

Three details here, each from a failed run:

- **Paths only in `tsconfig.json` are not enough.** We let `wingo-ui init` add the alias on its own: it wrote it to `tsconfig.json`, and `tsc -b` then failed with `Cannot find module '@/components/ui/button'`, because `tsconfig.app.json` never saw it.
- **No `baseUrl` on TypeScript 6.** With `"baseUrl": "."` in `tsconfig.app.json`, `tsc -b` stopped with error TS5101: the option is deprecated and stops working in TypeScript 7. `paths` resolves relative to the config file without it.
- **Add `node` to `types`.** The template limits global types to `vite/client`. Some components read `process.env.NODE_ENV` to print development warnings ([Theme Toggle](https://wingo-ui.com/components/theme-toggle) does), and `tsc` reports `Cannot find name 'process'` without it. Vite replaces the expression when it builds: our production bundle had no `process.env` left in it.

### 4. Run init

```bash
npx wingo-ui@latest init
```

It detected `vite`, found the alias in `tsconfig.app.json` (the CLI follows the `references` list) and reported `alias @/* -> src/`. Then it created `src/lib/utils.ts` with `cn`, installed `clsx` and `tailwind-merge`, merged the tokens into `src/index.css` with a `.dark` block and `@custom-variant dark (&:is(.dark *));`, and wrote `wingo-ui.json`.

If `package.json` lists `react-router` or `@react-router/dev`, init records the framework as `react-router` instead of `vite`, because it checks for React Router first. File locations and commands stay the same.

### 5. Add components

```bash
npx wingo-ui@latest add button card dialog
```

The free items install without an account. Files land in `src/components/ui`, `src/hooks` and `src/lib`, with the npm packages they import. Give the page its colors in `index.html`, since the starter CSS that styled `body` is gone:

```html
<body class="bg-background text-foreground antialiased">
```

Then use them:

```tsx
// src/App.tsx
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Dialog, DialogClose } from "@/components/ui/dialog"

export default function App() {
  return (
    <main className="mx-auto flex max-w-md flex-col gap-6 p-6">
      <Card
        title="Pro plan"
        description="Renews on November 1."
        footer={
          <Dialog
            trigger={<Button variant="soft">Cancel plan</Button>}
            title="Cancel your plan?"
            description="You keep Pro until the end of the billing period."
            footer={
              <>
                <DialogClose asChild>
                  <Button variant="soft">Keep plan</Button>
                </DialogClose>
                <Button tone="danger">Cancel plan</Button>
              </>
            }
          />
        }
      />
    </main>
  )
}
```

`npm run build` (which runs `tsc -b` first) passed with this file. [Button](https://wingo-ui.com/components/button), [Card](https://wingo-ui.com/components/card) and [Dialog](https://wingo-ui.com/components/dialog) carry `"use client"` at the top for the Next.js App Router; Vite ignores the directive. Press the button below to see its loading state.

> Live demo (Button): Click the button: it shows a spinner for a moment and keeps its width, so nothing next to it moves. Try it at [Button](https://wingo-ui.com/components/button) and install it with `npx wingo-ui@latest add button`.

## How do I get dark mode in Vite without Next.js?

The components follow a `.dark` class on `<html>`. Something has to set it before the first paint, and something has to change it when the user picks a theme. Our [Tailwind v4 dark mode guide](https://wingo-ui.com/blog/tailwind-v4-dark-mode) covers the `@custom-variant` line those components rely on.

**Before paint:** a script in `index.html`, in `<head>`, reads the saved choice and the system setting:

```html
<script>
  try {
    const theme = localStorage.getItem("theme")
    const dark = theme === "dark" || (theme !== "light" && matchMedia("(prefers-color-scheme: dark)").matches)
    document.documentElement.classList.toggle("dark", dark)
  } catch {}
</script>
```

**On change:** next-themes. Despite the name, version 0.4.6 depends on nothing but React, and it stores the choice under the same `theme` key the script reads:

```tsx
// src/main.tsx
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { ThemeProvider } from "next-themes"
import { UIProvider } from "@/lib/ui-config"
import "./index.css"
import App from "./App"

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
      <UIProvider locale="en-US">
        <App />
      </UIProvider>
    </ThemeProvider>
  </StrictMode>,
)
```

We loaded the production build in Chromium with Playwright in both color schemes: the `dark` class was on `<html>` before React started when the system preferred dark, the theme toggle flipped it, and `localStorage` kept the choice. [`UIProvider`](https://wingo-ui.com/components/ui-config) is optional: it sets app-wide component defaults and the locale for dates and numbers.

What uses next-themes in the library? As of October 13, 2026, only `theme-toggle.tsx` imports it, and the CLI installs it with that item. Theme Toggle (Pro) also takes `value` and `onValueChange` if you would rather keep the theme in your own state; then your code sets the class. Three navigation components, Bounce Sidebar, Hook Sidebar and Gooey Nav, import `next/link`, so they need Next.js. Everything else runs in Vite unchanged.

## Can I use the shadcn CLI in Vite instead?

Yes, with limits worth knowing first. We ran `npx shadcn@latest init -t vite -b radix -p nova` on a fresh project from step 3. Pick Radix: the [installation docs](https://wingo-ui.com/docs/installation) note that Base UI styles rewrite `asChild`, which these components rely on. Init 4.21.4 wrote `components.json`, `src/lib/utils.ts` and its own `src/components/ui/button.tsx`.

Free Wingo UI items install from their URL:

```bash
npx shadcn@latest add https://wingo-ui.com/r/dialog.json
```

The CLI asked to overwrite `utils.ts` and then `button.tsx`. Answer no to `utils.ts`: shadcn's version re-exports `cn` from the `cn` package, a drop-in for `clsx` plus `tailwind-merge`, and our files only need a `cn` export. Here is the catch. Keep shadcn's button and our dialog no longer type checks, because it passes props such as `radius` that shadcn's button does not have. Overwrite it and shadcn components that need its API break instead: shadcn's calendar imports `buttonVariants`, which our button does not export. The shadcn CLI also did not add our tokens to `src/index.css`, and it does not record versions, so `wingo-ui update` cannot merge later releases into those files.

For Pro items the docs add the `@wingo-ui` namespace to `components.json` with your key in an environment variable, then `npx shadcn@latest add @wingo-ui/calendar`. We did not test that path with a key for this post.

### The setup that worked: one folder per library

In the same shadcn project we ran `npx wingo-ui@latest init`, which added only the 43 token names shadcn had not defined, changed `"ui"` in `wingo-ui.json` to `"@/components/wingo"`, and ran `npx wingo-ui@latest add dialog`. Our files went to `src/components/wingo`, their imports were rewritten to match, shadcn's `button.tsx` stayed untouched, and `tsc -b` passed. The [CLI docs](https://wingo-ui.com/docs/cli) cover the other aliases.

## What should I check when something breaks?

| Error | Cause | Fix |
| --- | --- | --- |
| `Cannot find module '@/...'` from `tsc -b` | alias missing in `tsconfig.app.json` | add `paths` there (step 3) |
| `TS5101: Option 'baseUrl' is deprecated` | `baseUrl` on TypeScript 6 | remove it, keep `paths` |
| `Cannot find name 'process'` | `types` limited to `vite/client` | add `"node"` to `types` |
| Dark mode flashes light on reload | no script in `index.html` | add the pre-paint script |
| Components unstyled | tokens missing from `src/index.css` | run `npx wingo-ui@latest init` again |

## Next step

Run `npx wingo-ui@latest add button card dialog` in your new project and open the [installation docs](https://wingo-ui.com/docs/installation) for the manual path. The token system behind all of this is explained in the pillar on [Tailwind v4 design tokens](https://wingo-ui.com/blog/tailwind-v4-design-tokens), and more setup guides live under [Next.js and Tailwind CSS](https://wingo-ui.com/blog/category/nextjs-tailwind), including the [Next.js component library setup](https://wingo-ui.com/blog/next-js-component-library-setup) if you move the project to the App Router.

## Components in this post

- [Button](https://wingo-ui.com/components/button) (Free): The React button component every screen starts with: five variants, six tones, all sizes, and a loading state that never jumps. Install: `npx wingo-ui@latest add button`
- [Card](https://wingo-ui.com/components/card) (Free): A React card component for listings, stats, settings and plans: media, header, body and footer parts, and a lift-and-press mode. Install: `npx wingo-ui@latest add card`
- [Dialog](https://wingo-ui.com/components/dialog) (Free): A React modal dialog for focused tasks: a spring-scaled card on desktop and a bottom sheet on phones, with nesting and scrolling. Install: `npx wingo-ui@latest add dialog`
- [UIProvider](https://wingo-ui.com/components/ui-config) (Free): A React config provider for Wingo UI: app-wide default props for every component, the Intl locale and a reduced-motion policy. Install: `npx wingo-ui@latest add ui-config`

## FAQ

### Does shadcn work with Vite?

Yes. shadcn components are React and Tailwind source files with no Next.js dependency. Vite needs the @tailwindcss/vite plugin, an @ alias in vite.config.ts and matching paths in tsconfig.app.json.

### Why does tsc say it cannot find module @/components/ui/button in Vite?

The Vite template type checks src through tsconfig.app.json, which is a reference of tsconfig.json. Paths only in tsconfig.json do not reach it, so add the same paths entry for the @ alias, pointing at ./src, to tsconfig.app.json as well.

### Should I add baseUrl to tsconfig.app.json?

Not on TypeScript 6, which the Vite template installs as of October 2026. tsc -b fails with error TS5101 because baseUrl is deprecated, and paths work without it.

### Can I use next-themes in a Vite app?

Yes. next-themes 0.4.6 lists only react and react-dom as peer dependencies. Wrap the app in ThemeProvider with attribute="class" and add a small script to index.html so the class is set before the first paint.

### Can I install Wingo UI components with the shadcn CLI in Vite?

Free items install from their URL, for example npx shadcn@latest add https://wingo-ui.com/r/dialog.json. The shadcn CLI does not add the Wingo UI tokens or record versions, and a shadcn-made button.tsx clashes with ours, so the Wingo UI CLI with its own folder is the smoother path.

---

Source: https://wingo-ui.com/blog/shadcn-vite-setup
