shadcn-remix
GitHub

Installation

How to install and set up shadcn-remix in a Remix 3 app.

Create a Remix app

Start from the Remix 3 starter, or use an existing Remix 3 app.

$ pnpm dlx remix@latest new my-app

Install the headless primitives

Interactive components (dialogs, menus, selects, tabs, …) build on @remix-run/ui.

$ pnpm add @remix-run/ui

Add the global styles

The globals item adds app/ui/globals.css: the theme tokens, the Tailwind-equivalent reset, and the animation keyframes.

$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/globals.json

Serve it through your asset server and link it before anything else in <head>. It declares @layer base, rmx; so component styles always win over the reset.

app/actions/document.tsx
import { ImportMap } from 'remix/component/server'

import { assets, scriptEntry } from '../assets.ts'

const globalStylesHref = await assets.getHref('app/ui/globals.css')

export function Document(handle: Handle<{ children?: RemixNode }>) {
  return () => (
    <html lang="en">
      <head>
        <link rel="stylesheet" href={globalStylesHref} />
        <ImportMap value={scriptEntry.importMap} />
        <script type="module" src={scriptEntry.href}></script>
      </head>
      <body>{handle.props.children}</body>
    </html>
  )
}

Make app/ui browser-reachable

Components run in the browser when they hydrate, so allow app/ui (and @remix-run/ui) in your asset server, and add the #app/* import alias the examples use.

app/assets.ts
export const assets = createAssetServer({
  basePath: '/assets',
  allowFiles: ['app/routes.ts', 'app/**/public/**', 'app/ui/**'],
  allowPackages: ['remix', '@remix-run/ui'],
})
package.json
{
  "imports": {
    "#app/*": "./app/*"
  }
}

Add components

$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/button.json

Components that respond to events must render inside a clientEntry(...) boundary so they hydrate:

app/actions/public/settings.tsx
import { clientEntry, type Handle } from 'remix/component'

import { Switch } from '#app/ui/switch.tsx'

export const Settings = clientEntry(import.meta.url, function Settings(handle: Handle) {
  return () => <Switch defaultChecked />
})