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.
$ bunx --bun remix@latest new my-app$ npx remix@latest new my-app$ pnpm dlx remix@latest new my-app$ yarn dlx remix@latest new my-appInstall the headless primitives
Interactive components (dialogs, menus, selects, tabs, …) build on @remix-run/ui.
$ bun add @remix-run/ui$ npm install @remix-run/ui$ pnpm add @remix-run/ui$ yarn add @remix-run/uiAdd the global styles
The globals item adds app/ui/globals.css: the theme tokens, the Tailwind-equivalent reset, and the animation keyframes.
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/globals.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/globals.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/globals.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/globals.jsonServe 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.
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.
export const assets = createAssetServer({
basePath: '/assets',
allowFiles: ['app/routes.ts', 'app/**/public/**', 'app/ui/**'],
allowPackages: ['remix', '@remix-run/ui'],
}){
"imports": {
"#app/*": "./app/*"
}
}Add components
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/button.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/button.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/button.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/button.jsonComponents that respond to events must render inside a clientEntry(...) boundary so they hydrate:
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 />
})