shadcn setup
components.json and the CLI.
Join UI is a shadcn registry, so the CLI is the same one you already use. This page covers the configuration it reads.
Initialise
pnpm dlx shadcn@latest initThis writes components.json, creates lib/utils.ts with the cn helper, and
adds the base tokens to your stylesheet. Join UI components need all three.
components.json
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@joinui": "https://ui.join-way.com/r/{name}.json"
}
}The fields Join UI actually depends on:
| Field | Why it matters |
|---|---|
tailwind.config | Empty string for Tailwind v4 — there is no config file |
tailwind.cssVariables | Must be true; components theme through variables only |
aliases.utils | Where cn lives. Components import it as @/lib/utils |
rsc | Controls whether the CLI keeps the "use client" directive |
registries | Maps the @joinui namespace to the item URL template |
The cn utility
Every registry item declares utils in registryDependencies, so the CLI
installs this for you if it is missing:
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}twMerge is not optional. Components put their defaults first and className
last, and twMerge is what makes a caller's rounded-full actually beat the
component's rounded-lg.
Adding components
By namespace, substituting the slug from the catalog:
pnpm dlx shadcn@latest add @joinui/<component>Several at once:
pnpm dlx shadcn@latest add @joinui/<first> @joinui/<second>By URL, with no namespace configured:
pnpm dlx shadcn@latest add https://ui.join-way.com/r/<component>.jsonMixed with shadcn's own registry — namespaced and bare names resolve independently:
pnpm dlx shadcn@latest add button dialog @joinui/<component>What the CLI does
Fetches the item
GET https://ui.join-way.com/r/<name>.json. The response is served with
permissive CORS headers, so it works from any origin.
Resolves registryDependencies
Join UI items depend on utils, which resolves against shadcn's own
registry. A dependency written with the @joinui prefix resolves against
the namespace you configured instead.
Installs npm dependencies
Detected from your lockfile — pnpm, npm, yarn or bun.
Writes the files
Each file has a target, so it lands at
components/joinui/<name>.tsx regardless of your components alias.
Coexisting with shadcn/ui
Join UI components are deliberately standalone: none of them import
@/components/ui/*. They live in components/joinui/, which means:
- adding shadcn components will never overwrite one;
pnpm dlx shadcn@latest add buttonupgrading your Button will not touch them;- you can delete the whole
components/joinui/directory to remove every trace.
They do read your tokens, so they inherit your theme automatically.