Installation

Start with a Preact 11 project with Tailwind CSS v4 configured. Follow the Tailwind Vite guide if you need to add Tailwind.

  1. 1. Configure Preact compatibility

    With Vite, use @preact/preset-vite. It aliases React imports to Preact for the upstream libraries. Keep the same aliases in your TypeScript configuration.

    // vite.config.ts — keep your existing Tailwind plugin
    import preact from "@preact/preset-vite";
    import { defineConfig } from "vite";
    
    export default defineConfig({
      plugins: [preact()],
    });
    // tsconfig.json — merge into compilerOptions
    {
      "compilerOptions": {
        "jsx": "react-jsx",
        "jsxImportSource": "preact",
        "paths": {
          "react": ["./node_modules/preact/compat/"],
          "react-dom": ["./node_modules/preact/compat/"],
          "react/jsx-runtime": ["./node_modules/preact/jsx-runtime/"],
          "react/jsx-dev-runtime": ["./node_modules/preact/jsx-runtime/"]
        }
      }
    }
  2. 2. Add shared dependencies

    bun add --exact class-variance-authority@0.7.1 cn@0.4.0 lucide-preact@1.51.0 @floating-ui/react-dom@2.1.9 tw-animate-css@1.4.0
  3. 3. Choose your components

    Choose a component from the catalog and follow its installation guide to copy that component and its local dependencies directly from the repository with degit.

    Copy the entire catalog instead
    bunx degit https://github.com/LiasCode/shadcn-preact/registry/ui#main ./src/components/ui

    This copies the full catalog. Keep lib/ and primitives/ alongside the wrappers: their imports are relative. You can remove components you do not use, retaining the files they import.

  4. 4. Add the styles

    Copy shadcn-tailwind.css into src/styles/. Add these imports at the top of your global stylesheet.

    /* src/index.css */
    @import "tailwindcss";
    @import "tw-animate-css";
    @import "./styles/shadcn-tailwind.css";
    
    @custom-variant dark (&:is(.dark *));

    Add the theme tokens below after the imports. These match this port's nova styles. Import your global stylesheet from the app entry point.

    Show theme CSS
    @theme inline {
      --font-heading: var(--font-sans);
      --font-sans: "Geist Variable", sans-serif;
      --color-sidebar-ring: var(--sidebar-ring);
      --color-sidebar-border: var(--sidebar-border);
      --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
      --color-sidebar-accent: var(--sidebar-accent);
      --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
      --color-sidebar-primary: var(--sidebar-primary);
      --color-sidebar-foreground: var(--sidebar-foreground);
      --color-sidebar: var(--sidebar);
      --color-chart-5: var(--chart-5);
      --color-chart-4: var(--chart-4);
      --color-chart-3: var(--chart-3);
      --color-chart-2: var(--chart-2);
      --color-chart-1: var(--chart-1);
      --color-ring: var(--ring);
      --color-input: var(--input);
      --color-border: var(--border);
      --color-destructive: var(--destructive);
      --color-accent-foreground: var(--accent-foreground);
      --color-accent: var(--accent);
      --color-muted-foreground: var(--muted-foreground);
      --color-muted: var(--muted);
      --color-secondary-foreground: var(--secondary-foreground);
      --color-secondary: var(--secondary);
      --color-primary-foreground: var(--primary-foreground);
      --color-primary: var(--primary);
      --color-popover-foreground: var(--popover-foreground);
      --color-popover: var(--popover);
      --color-card-foreground: var(--card-foreground);
      --color-card: var(--card);
      --color-foreground: var(--foreground);
      --color-background: var(--background);
      --radius-sm: calc(var(--radius) * 0.6);
      --radius-md: calc(var(--radius) * 0.8);
      --radius-lg: var(--radius);
      --radius-xl: calc(var(--radius) * 1.4);
      --radius-2xl: calc(var(--radius) * 1.8);
      --radius-3xl: calc(var(--radius) * 2.2);
      --radius-4xl: calc(var(--radius) * 2.6);
    }
    
    :root {
      --background: oklch(1 0 0);
      --foreground: oklch(0.145 0 0);
      --card: oklch(1 0 0);
      --card-foreground: oklch(0.145 0 0);
      --popover: oklch(1 0 0);
      --popover-foreground: oklch(0.145 0 0);
      --primary: oklch(0.205 0 0);
      --primary-foreground: oklch(0.985 0 0);
      --secondary: oklch(0.97 0 0);
      --secondary-foreground: oklch(0.205 0 0);
      --muted: oklch(0.97 0 0);
      --muted-foreground: oklch(0.556 0 0);
      --accent: oklch(0.97 0 0);
      --accent-foreground: oklch(0.205 0 0);
      --destructive: oklch(0.577 0.245 27.325);
      --border: oklch(0.922 0 0);
      --input: oklch(0.922 0 0);
      --ring: oklch(0.708 0 0);
      --chart-1: oklch(0.87 0 0);
      --chart-2: oklch(0.556 0 0);
      --chart-3: oklch(0.439 0 0);
      --chart-4: oklch(0.371 0 0);
      --chart-5: oklch(0.269 0 0);
      --radius: 0.625rem;
      --sidebar: oklch(0.985 0 0);
      --sidebar-foreground: oklch(0.145 0 0);
      --sidebar-primary: oklch(0.205 0 0);
      --sidebar-primary-foreground: oklch(0.985 0 0);
      --sidebar-accent: oklch(0.97 0 0);
      --sidebar-accent-foreground: oklch(0.205 0 0);
      --sidebar-border: oklch(0.922 0 0);
      --sidebar-ring: oklch(0.708 0 0);
    }
    
    .dark {
      --background: oklch(0.145 0 0);
      --foreground: oklch(0.985 0 0);
      --card: oklch(0.205 0 0);
      --card-foreground: oklch(0.985 0 0);
      --popover: oklch(0.205 0 0);
      --popover-foreground: oklch(0.985 0 0);
      --primary: oklch(0.922 0 0);
      --primary-foreground: oklch(0.205 0 0);
      --secondary: oklch(0.269 0 0);
      --secondary-foreground: oklch(0.985 0 0);
      --muted: oklch(0.269 0 0);
      --muted-foreground: oklch(0.708 0 0);
      --accent: oklch(0.269 0 0);
      --accent-foreground: oklch(0.985 0 0);
      --destructive: oklch(0.704 0.191 22.216);
      --border: oklch(1 0 0 / 10%);
      --input: oklch(1 0 0 / 15%);
      --ring: oklch(0.556 0 0);
      --chart-1: oklch(0.87 0 0);
      --chart-2: oklch(0.556 0 0);
      --chart-3: oklch(0.439 0 0);
      --chart-4: oklch(0.371 0 0);
      --chart-5: oklch(0.269 0 0);
      --sidebar: oklch(0.205 0 0);
      --sidebar-foreground: oklch(0.985 0 0);
      --sidebar-primary: oklch(0.488 0.243 264.376);
      --sidebar-primary-foreground: oklch(0.985 0 0);
      --sidebar-accent: oklch(0.269 0 0);
      --sidebar-accent-foreground: oklch(0.985 0 0);
      --sidebar-border: oklch(1 0 0 / 10%);
      --sidebar-ring: oklch(0.556 0 0);
    }
    
    @layer base {
      * {
        @apply border-border outline-ring/50;
      }
      body {
        @apply bg-background text-foreground;
      }
    }

Usage

Import a copied component and use it in your app.

import { Button } from "./components/ui/button";

export function App() {
  return <Button>Continue</Button>;
}

For dark mode, wrap your app in ThemeProvider from ./components/ui/theme. Use useTheme to switch between light, dark, and system. The provider applies the dark class to the document element.

Additional dependencies

Install these only when you use the corresponding component.

Component-specific runtime dependencies
ComponentPackage
Calendarreact-day-picker, date-fns (date examples)
Chartrecharts
Commandcmdk
Carouselembla-carousel-react, embla-carousel-autoplay (optional)
Input OTPinput-otp
Resizablereact-resizable-panels 4.x
Sonnersonner

Combobox, Toast, Message Scroller, and Questionnaire use local Preact primitives.

Upstream documentation

Use the official Base UI component documentation for composition and usage patterns. Adapt imports to your local Preact components. The official CLI installs upstream components; use the copy command above for this port.

For customization, see Theming. The older release is maintained on the v3 branch.