Install MiniDev UI in React Router (framework mode)
Install MiniDev UI in a React Router v7 framework mode app: Tailwind v4, the ~ alias, shadcn init, the tokens stylesheet, SSR safe dark mode and npm.
To add MiniDev UI to React Router in framework mode, create the app with create-react-router (it already configures Tailwind CSS v4 and the ~/* alias), run npx shadcn@latest init, and import the MiniDev tokens stylesheet after Tailwind in app/app.css. Then add components with npx shadcn@latest add https://ui.minidev.pro/r/button.json and import them from ~/components/ui.
Step 1: Create the app
npx create-react-router@latest my-app
cd my-app
The default template is framework mode with server rendering on. Routes live in app/routes, the document shell in app/root.tsx.
Step 2: Check Tailwind CSS v4
The template ships Tailwind v4 through the Vite plugin. If your project predates that, install it and add the plugin before reactRouter():
npm install tailwindcss @tailwindcss/vite
import { reactRouter } from "@react-router/dev/vite"
import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "vite"
import tsconfigPaths from "vite-tsconfig-paths"
export default defineConfig({
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
})
vite-tsconfig-paths reads the alias from tsconfig.json, so there is no separate resolve.alias to maintain.
Step 3: Run shadcn init
The template maps ~/* to ./app/*. The shadcn CLI picks that up, so components land in app/components/ui and import as ~/components/ui/button.
npx shadcn@latest init
Init writes components.json, creates app/lib/utils.ts with cn, installs clsx and tailwind-merge, and adds a theme to app/app.css. You replace that theme next.
Step 4: Add the MiniDev tokens stylesheet
MiniDev components read semantic tokens such as bg-surface, text-fg-muted and shadow-raised from one stylesheet, served at https://ui.minidev.pro/r/styles.css:
curl -o app/minidev.css https://ui.minidev.pro/r/styles.css
@import "tailwindcss";
/* keep any other @import lines shadcn init added, such as tw-animate-css */
@import "./minidev.css";
Delete everything else in app/app.css: the template's own @theme font and html, body background rules, and the :root, .dark and @theme inline blocks from init. Those blocks set --accent, --border and --ring to neutral values and override MiniDev's tokens, so the accent turns gray. MiniDev's stylesheet already maps every shadcn variable name.
The stylesheet begins with @custom-variant dark (&:is(.dark *));, so dark mode follows a dark class on <html>, not the OS; remove the duplicate from init. Fonts read --font-geist-sans and --font-geist-mono. Swap the template's font link in app/root.tsx for Geist and define the variables:
export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@100..900&display=swap",
},
]
:root {
--font-geist-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
--font-geist-mono: "Geist Mono", ui-monospace, monospace;
}
Optional: the stylesheet ships four materials. Hairline is the default; set data-material="glass", "metal" or "paper" on <html> in the Layout export, or on any element, to restyle the page or one section without touching a component.
Step 5: Add components with the shadcn CLI
npx shadcn@latest add https://ui.minidev.pro/r/button.json
npx shadcn@latest add https://ui.minidev.pro/r/pricing-plans.json
The first command writes app/components/ui/button.tsx and installs @base-ui/react and class-variance-authority. The second resolves registry dependencies: pricing-plans lists button, number-roll and segmented-control by full MiniDev URL, so the CLI fetches each and installs lucide-react and motion. Decline overwriting button.tsx if you changed it.
Step 6: Use it in a route
import { useNavigate } from "react-router"
import { Button } from "~/components/ui/button"
import { PricingPlans } from "~/components/ui/pricing-plans"
export default function Home() {
const navigate = useNavigate()
return (
<main className="mx-auto max-w-5xl space-y-10 p-8">
<Button variant="accent">Start free</Button>
<PricingPlans onSelect={(plan, period) => navigate(`/signup?plan=${plan}&period=${period}`)} />
</main>
)
}
Run npm run dev. The route renders on the server and hydrates in the browser. Unlike Next.js, route components are ordinary React components, so you can pass handlers directly.
Alternative: the npm package
npm i minidev-ui-kit @base-ui/react class-variance-authority clsx tailwind-merge lucide-react motion
@import "tailwindcss";
@import "minidev-ui-kit/styles.css";
@source "../node_modules/minidev-ui-kit";
Import by path, for example minidev-ui-kit/ui/button. @source is required because Tailwind skips node_modules; its path is relative to app/app.css. The font setup above still applies.
Dark mode without a flash
Server rendered HTML does not know the visitor's choice, so set the class with an inline script in the Layout export before the body paints, and add suppressHydrationWarning to <html>:
const themeScript = "(function(){try{var t=localStorage.getItem('theme');var d=t?t==='dark':matchMedia('(prefers-color-scheme: dark)').matches;if(d)document.documentElement.classList.add('dark')}catch(e){}})()"
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
)
}
The same approach, with a toggle and system preference, is covered in depth in Dark mode in Next.js without the flash; the script is identical.
React Router specific gotchas
- The alias is
~, not@. Code copied from other MiniDev docs that imports@/components/ui/...needs the prefix changed. The CLI rewrites imports inside the files it installs for you. "use client"at the top of each component is ignored in the default SSR mode. It only matters if you opt into React Router's React Server Components support.- Mount
<LightProvider />(fromhttps://ui.minidev.pro/r/light-provider.json) insideLayoutorAppif you want shadows to follow the pointer. It only touchesdocumentin an effect, so it is SSR safe.
Troubleshooting
- `Cannot find module "~/components/ui/button"`.
tsconfigPaths()is missing fromvite.config.ts, orcomponents.jsonpoints at a different alias. - Text renders in a system font or Inter. The template's
@themefont rule is still inapp/app.css, or the Geist variables are not defined. - npm route: `Unknown file extension ".tsx"` during SSR. Add
ssr: { noExternal: ["minidev-ui-kit"] }tovite.config.tsso Vite compiles the package instead of handing it to Node. - Hydration warning on `<html>`.
suppressHydrationWarningis missing from theLayoutexport.
Frequently asked questions
Does this work in React Router declarative or data mode?
Yes. Without the framework plugin you have a plain Vite app, so follow the React with Vite install steps and use the @/* alias instead of ~/*.
Can I use MiniDev UI with Remix?
Remix v2 became React Router v7, and this guide is the upgrade path. On a Remix v2 app that already uses Vite and Tailwind v4, the same steps apply; only the route imports come from @remix-run/react.
Do MiniDev components need a client-only wrapper?
No. They render on the server and hydrate. Browser-only work, such as reading the pointer or localStorage, happens inside effects.
Where should a theme toggle live?
Anywhere below Layout. It only needs to flip the dark class on document.documentElement and write theme to localStorage, so the inline script picks up the choice on the next request.