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

bash
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():

bash
npm install tailwindcss @tailwindcss/vite
vite.config.tsts
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.

bash
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:

bash
curl -o app/minidev.css https://ui.minidev.pro/r/styles.css
app/app.csscss
@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:

app/root.tsxtsx
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",
  },
]
app/app.csscss
: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

bash
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.

ButtonuiShadcn compatible React button on Base UI with eight variants, nine sizes including icon sizes, a pressed state and render prop support for links.npx shadcn@latest add ui.minidev.pro/r/button.jsonPricing PlansblockA React pricing section with a monthly or yearly switch that rolls prices to the new value, a featured ink plan, and feature rows marked included or not.npx shadcn@latest add ui.minidev.pro/r/pricing-plans.json

Step 6: Use it in a route

app/routes/home.tsxtsx
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

bash
npm i minidev-ui-kit @base-ui/react class-variance-authority clsx tailwind-merge lucide-react motion
app/app.csscss
@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>:

app/root.tsxtsx
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 /> (from https://ui.minidev.pro/r/light-provider.json) inside Layout or App if you want shadows to follow the pointer. It only touches document in an effect, so it is SSR safe.

Troubleshooting

  • `Cannot find module "~/components/ui/button"`. tsconfigPaths() is missing from vite.config.ts, or components.json points at a different alias.
  • Text renders in a system font or Inter. The template's @theme font rule is still in app/app.css, or the Geist variables are not defined.
  • npm route: `Unknown file extension ".tsx"` during SSR. Add ssr: { noExternal: ["minidev-ui-kit"] } to vite.config.ts so Vite compiles the package instead of handing it to Node.
  • Hydration warning on `<html>`. suppressHydrationWarning is missing from the Layout export.

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.

Other frameworks