Install MiniDev UI in Astro (React islands)
Use MiniDev UI in Astro as React islands: the React integration, Tailwind v4, shadcn init, the tokens stylesheet, client directives and dark mode with no flash.
To add MiniDev UI to Astro, enable the React integration and Tailwind CSS v4, set the @/* alias, run npx shadcn@latest init, and import the MiniDev tokens stylesheet after Tailwind in your global CSS. Components then render as React islands: static HTML by default, and interactive once you add a client directive such as client:load.
Step 1: Create the project
npm create astro@latest my-app -- --template with-tailwindcss --install --add react --git
cd my-app
This starts from Astro's Tailwind template and adds @astrojs/react. In an existing project, run npx astro add react instead.
Step 2: Check Tailwind CSS v4
Astro uses Tailwind v4 through the Vite plugin. The template already has it. To add it by hand:
npm install tailwindcss @tailwindcss/vite
import { defineConfig } from "astro/config"
import react from "@astrojs/react"
import tailwindcss from "@tailwindcss/vite"
export default defineConfig({
integrations: [react()],
vite: {
plugins: [tailwindcss()],
},
})
Global styles live in src/styles/global.css, starting with @import "tailwindcss";, and are imported by your layout.
Step 3: Set the alias and run shadcn init
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Keep the extends, include and exclude keys Astro generated; add only compilerOptions. Astro's Vite setup reads these paths, so no separate alias config is needed.
npx shadcn@latest init
Init writes components.json, creates src/lib/utils.ts with cn, and adds a theme to src/styles/global.css, which the next step replaces.
Step 4: Add the MiniDev tokens stylesheet
MiniDev components read semantic tokens like bg-surface and shadow-raised from one stylesheet, served at https://ui.minidev.pro/r/styles.css:
curl -o src/styles/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";
:root {
--font-geist-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
--font-geist-mono: "Geist Mono", ui-monospace, monospace;
}
Delete the :root, .dark and @theme inline blocks init generated. They 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. Keep only the font variables shown above.
The stylesheet begins with @custom-variant dark (&:is(.dark *));, so dark mode follows a dark class on <html>; remove the duplicate from init. The font variables need Geist itself, so load it in the layout <head>, for example from Google Fonts with family=Geist:wght@100..900&family=Geist+Mono:wght@100..900. Without them text falls back to serif.
Optional: the stylesheet ships four materials. Hairline is the default; set data-material="glass", "metal" or "paper" on <html> in your layout, or on any element, to restyle the page or one section without touching a component. This works in static .astro markup too.
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 writes src/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.
Step 6: Use it in a page
---
import "@/styles/global.css"
import { Button } from "@/components/ui/button"
import { PricingPlans } from "@/components/ui/pricing-plans"
---
<main class="mx-auto max-w-5xl space-y-10 p-8">
<form method="post" action="/api/waitlist">
<Button type="submit" variant="accent">Join the waitlist</Button>
</form>
<PricingPlans client:load />
</main>
Button here has no directive, so Astro renders it to HTML and ships no JavaScript for it. That is fine for a submit button in a plain form. PricingPlans keeps the billing period in React state, so it needs client:load (or client:visible to hydrate when it scrolls into view). Without a directive the toggle renders but does nothing.
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, such as minidev-ui-kit/ui/button. @source is relative to the CSS file, hence two levels up from src/styles. The font variables still apply.
Dark mode without a flash
Put an inline script in the layout <head>. is:inline stops Astro from bundling it, so it runs before first paint:
<head>
<script is:inline>
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) {}
</script>
</head>
With view transitions (<ClientRouter />), <html> attributes are replaced on navigation. Run the same logic again in a document.addEventListener("astro:after-swap", ...) listener.
Astro specific gotchas
- Interactive components need a client directive. Menus, dialogs, tabs, toggles and anything with state or handlers must be hydrated with
client:load,client:idleorclient:visible. - Props must be serializable. You cannot pass a function such as
onSelectfrom an.astrofile. Write a small.tsxwrapper that defines the handler, and hydrate the wrapper. - Each island is its own React root. Context does not cross islands, so compose multi-part components (a dialog and its trigger, a form and its fields) inside one
.tsxfile. - Use `className` on React components.
classworks on HTML in.astromarkup; MiniDev components expectclassName. - Light provider. For shadows that follow the pointer, add
https://ui.minidev.pro/r/light-provider.jsonand render<LightProvider client:idle />once in the layout. It renders nothing and writes CSS variables to<html>.
Troubleshooting
- Components render unstyled. The page or layout does not import
global.css, or MiniDev is imported before Tailwind. - A toggle or menu does not respond. It is missing a client directive.
- `Invalid hook call` or a blank island.
@astrojs/reactis not inintegrations. - npm route: `Unknown file extension ".tsx"` during build. Add
vite: { ssr: { noExternal: ["minidev-ui-kit"] } }toastro.config.mjs.
Frequently asked questions
Do MiniDev components ship JavaScript in Astro?
Only when you hydrate them. Without a client directive a component is rendered to static HTML at build or request time, and its styles still apply.
Which client directive should I use?
client:load for anything visible and interactive on arrival, client:visible for below-the-fold blocks such as pricing, and client:idle for background pieces like the light provider.
Can I use MiniDev UI with other frameworks in the same Astro site?
Yes. MiniDev components are React, but the tokens stylesheet is plain CSS, so Svelte or Vue islands and .astro markup can use the same utilities such as bg-surface and text-fg-muted.
Does this work with static output?
Yes. With the default static build, islands hydrate from prerendered HTML and the inline dark mode script runs in the browser as usual. Server output and adapters need no extra MiniDev setup.