Skip to content
Join UI

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

bash
pnpm dlx shadcn@latest init

This 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

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:

FieldWhy it matters
tailwind.configEmpty string for Tailwind v4 — there is no config file
tailwind.cssVariablesMust be true; components theme through variables only
aliases.utilsWhere cn lives. Components import it as @/lib/utils
rscControls whether the CLI keeps the "use client" directive
registriesMaps 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:

lib/utils.ts
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:

bash
pnpm dlx shadcn@latest add @joinui/<first> @joinui/<second>

By URL, with no namespace configured:

bash
pnpm dlx shadcn@latest add https://ui.join-way.com/r/<component>.json

Mixed with shadcn's own registry — namespaced and bare names resolve independently:

bash
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 button upgrading 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.