shadcn.io is an independent project built on shadcn/ui
React Router 7 in framework mode is Remix's successor: routes with loaders and actions, server rendering, built on Vite. Its starter already has Tailwind CSS v4 and an import alias, ~/, so setup is init and a first component.
Create a new app
Run init with the React Router template
The CLI asks for a project name and creates the app from shadcn's React Router starter (React Router 7, React 19, Vite 8, Tailwind CSS v4), then adds our theme to app/app.css. Add --monorepo for a workspace with the app in apps/web and the components in packages/ui.
Add a component
The starter's home route already renders a Button: answer yes when the CLI asks to overwrite button.tsx.
Use it in a route
import { Button } from "~/components/ui/button";
export default function Home() {
return (
<div className="flex min-h-svh items-center justify-center">
<Button>Click me</Button>
</div>
);
}Add it to an existing app
Create the app, if you need one
bunx --bun create-react-router@latest my-appIts default template sets up Tailwind CSS v4 (in app/app.css, through @tailwindcss/vite) and the ~/* alias, which points at app/.
Check the alias
An app from an older template may not have it. Add it to tsconfig.json:
{
"compilerOptions": {
"paths": {
"~/*": ["./app/*"]
}
}
}Vite resolves it too when vite.config.ts has resolve: { tsconfigPaths: true } (Vite 8) or the vite-tsconfig-paths plugin, as the templates do.
Run init
The CLI reads the ~/* alias, so components go to app/components/ui and import as ~/components/ui/…. The theme goes into app/app.css, which app/root.tsx imports.
Add a component and use it
Import it from ~/components/ui/dialog in any route.
Data from a loader
Components render on the server and hydrate in the browser, so they take a loader's data like any other markup. A table of invoices from a loader:
import type { Route } from "./+types/invoices";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
export async function loader() {
return {
invoices: [
{ id: "INV001", status: "Paid", total: "$250.00" },
{ id: "INV002", status: "Pending", total: "$150.00" },
],
};
}
export default function Invoices({ loaderData }: Route.ComponentProps) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-end">Total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loaderData.invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell>{invoice.id}</TableCell>
<TableCell>{invoice.status}</TableCell>
<TableCell className="text-end">{invoice.total}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
Register the route in app/routes.ts with route("invoices", "routes/invoices.tsx"). For forms that post to an action, build them with Field, as the Form guide shows.
FAQ
Why do imports start with ~/ instead of @/?
React Router's templates name their alias ~/*, pointing at app/. init reads the alias from tsconfig.json and writes components with it, so you import ~/components/ui/button, the same way the app's own files import each other.
Does it work in SPA mode, with ssr set to false?
Yes. The components don't depend on server rendering: with ssr: false in react-router.config.ts, they render in the browser like in a Vite app. Setup is the same.
I use React Router as a library, not the framework. Which guide is mine?
Declarative or data mode inside your own bundler: follow the guide for that bundler, usually Vite. The components don't care which router renders them.
Is this the guide for Remix?
Yes, for new apps. Remix v2's framework became React Router 7, and new projects start here. A Remix v2 app has an upgrade path first: see the Remix guide.
Why do I see a hydration warning with dark mode?
The dark class is set in the browser before React hydrates, so the server's <html> differs from the client's. Set the theme from a cookie in the root loader so the server renders the same class, or add suppressHydrationWarning to <html> in app/root.tsx.
Where does the theme go?
Into app/app.css, the stylesheet the template imports from app/root.tsx. init finds it from components.json's tailwind.css setting, which it fills in on the first run.