Installation
Set up a Remix 3 app for Stems components and add your first one.
Verified on Nov 1, 2026 against a fresh remix new app (remix@3.1.0,
shadcn@4.23.0, tailwindcss@4.3.3).
Stems components are copied into your app with the shadcn CLI, like shadcn/ui. A Remix 3 app needs five one-time setup steps first, because Remix serves unbundled TypeScript to the browser and Node needs real file extensions.
1. Add an #app/* import alias
shadcn components import shared files through aliases. Remix has no bundler alias support, but Node
subpath imports work on the server and
in Remix's asset server. Add to package.json:
{
"imports": {
"#app/*": "./app/*"
}
}
2. Remove rewriteRelativeImportExtensions from tsconfig.json
The starter enables it, but it only matters when TypeScript emits files, and Remix apps use
noEmit. With it enabled, TypeScript rejects #app/....ts imports (TS2877).
- "rewriteRelativeImportExtensions": true,
3. Let the asset server serve components and their packages
In app/assets.ts:
allowFiles: ['app/routes.ts', 'app/**/public/**', 'app/ui/**', 'app/lib/**'],
allowPackages: ['remix', 'stems-ui', 'clsx', 'tailwind-merge'],
4. Set up Tailwind CSS v4
npm i -D tailwindcss @tailwindcss/cli
app/styles/app.css:
@import 'tailwindcss';
Build it to public/styles.css (add --watch in development, e.g. alongside npm run dev):
npx tailwindcss -i app/styles/app.css -o public/styles.css
and link it in your document <head>:
<link rel="stylesheet" href="/styles.css" />
5. Add components.json
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-vega",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/styles/app.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "#app/ui",
"ui": "#app/ui",
"utils": "#app/lib/utils",
"lib": "#app/lib",
"hooks": "#app/hooks"
},
"registries": {
"@stemsui": "https://stemsui.com/r/{name}.json"
}
}
To try unreleased components from a local checkout, run the docs app (pnpm dev in this repo)
and point @stemsui at http://localhost:44100/r/{name}.json instead.
Add components
npx shadcn add @stemsui/theme @stemsui/dropdown-menu
This writes app/ui/dropdown-menu.tsx and app/lib/utils.ts, installs stems-ui,
clsx, and tailwind-merge, and merges the theme variables into app/styles/app.css. The theme
item also installs tw-animate-css and shadcn and adds @import "tw-animate-css" and
@import "shadcn/tailwind.css", the same imports shadcn init adds, so shadcn's animation and
data-* variant classes work. shadcn rewrites
imports to your aliases and keeps the .ts/.tsx extensions Remix needs, e.g.
import { cn } from '#app/lib/utils.ts'.
Use them
Interactive components must render inside a clientEntry(...) boundary:
import { clientEntry, type Handle } from 'remix/component'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
onMenuSelect,
} from '#app/ui/dropdown-menu.tsx'
export const ActionsMenu = clientEntry(import.meta.url, function ActionsMenu(_handle: Handle) {
return () => (
<div mix={onMenuSelect((event) => console.log(event.value))}>
<DropdownMenu>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem value="edit">Edit</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
})
Possible future simplification
Steps 1–5 could become a single npx shadcn add @stemsui/init item (shadcn items can ship
package.json-adjacent files and CSS), or a remix doctor-style check. Tracked in PLAN.md.