Contributing
Add a component end to end.
Adding a component touches four files. Everything else — the catalog card, the sidebar entry, search, the registry item, the AI prompt, the sitemap — is derived.
Local setup
git clone https://github.com/d1maash/join-ui.git
cd join-ui
pnpm install
pnpm devAdding a component
Write the component
Create registry/components/<slug>.tsx. This file ships to other people's
projects, so it may only import from react, its declared npm
dependencies, and @/lib/utils.
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
export interface GlowCardProps extends React.ComponentPropsWithoutRef<"div"> {
intensity?: number
}
export function GlowCard({ intensity = 1, className, ...props }: GlowCardProps) {
return (
<div
className={cn("border border-border bg-card p-5", className)}
{...props}
/>
)
}The house rules:
- Theme through tokens. No
dark:variants — see dark mode for why. - Colour is welcome, but only through the
--info/--positive/--caution/--criticalfamilies. No literal colours and nothing from Tailwind's built-in palette — see theming. - Elevation comes from the
--elevation-*steps, reached asshadow-xsthroughshadow-lg. Do not invent a shadow, and do not use a gradient as one. - Round through
rounded-soft-sm,rounded-softandrounded-soft-lgrather than the plain scale, so a project that retunes its own radii does not resize your component. - Never let colour be the only carrier of state: pair it with a glyph and a label.
- Accept and merge
classNamethroughcn, always last. - Export the props interface.
- Animate
transformandopacityonly, and handleprefers-reduced-motion. - Mark
"use client"only if the component genuinely needs state, refs, effects or browser APIs.
Write a preview
Create components/previews/<slug>-preview.tsx with a default export. This
one never leaves the site, so it may import anything.
"use client"
import { GlowCard } from "@/registry/components/glow-card"
export default function GlowCardPreview() {
return <GlowCard className="max-w-sm">Preview content</GlowCard>
}Register it so it code-splits. The map holds render functions, not
component types — previews is a Record<string, () => ReactNode>, and
renderPreview(slug) calls the entry:
const GlowCardPreview = dynamic(() => import("./glow-card-preview"))
export const previews: Record<string, () => ReactNode> = {
"glow-card": () => <GlowCardPreview />,
}Storing a function rather than a component keeps the preview frame from instantiating a value it received at runtime, and leaves the entry free to pass props or wrap the demo in a stage. A slug with no entry renders an explicit empty state rather than failing.
Describe it
Add one entry to lib/registry/components.ts. This is the single source of
truth: the component page, catalog card, search index, sidebar link,
registry item and AI prompt are all projected from it.
defineComponent({
name: "GlowCard",
slug: "glow-card",
title: "Glow Card",
description: "A card with a soft animated glow.",
overview: "Two to four sentences. Reused on the page and in the AI prompt.",
category: "Cards",
tags: ["card", "surface"],
status: "new",
featured: false,
dependencies: [],
registryDependencies: ["utils"],
files: [uiFile("glow-card")],
accessibility: ["Decorative layers are aria-hidden and pointer-events-none."],
keyboard: [],
props: [
{
name: "GlowCard",
props: [
{
name: "intensity",
type: "number",
defaultValue: "1",
description: "Glow strength.",
},
],
},
],
usage: `import { GlowCard } from "@/components/joinui/glow-card"
export function Example() {
return <GlowCard>Content</GlowCard>
}`,
customization: [],
related: [],
since: "2026-08-02",
})installCommand is derived by defineComponent — never write it by hand.
related takes slugs of other components and is validated, so leave it
empty until the components it would point at exist.
Validate and build
pnpm registry:validate
pnpm registry:build
pnpm checkValidation fails loudly if the file does not exist, the preview is not
registered, related points at an unknown slug, the slug is not kebab-case,
or the description is longer than 160 characters.
Adding a documentation page
Two files, again derived from one source.
Add the MDX
Create content/docs/<slug>.mdx. No frontmatter — the title and description
live in the navigation.
Register it
{
title: "Guides",
items: [
item("my-page", "My page", "One-line description for search and metadata."),
],
}That entry drives the sidebar, breadcrumbs, previous/next links, the search
index, the sitemap and generateStaticParams.
Available MDX components
| Component | Use |
|---|---|
<Callout variant="note | tip | warning | danger"> | Highlighted aside |
<Steps> / <Step title="…"> | Numbered walkthrough |
<CardGroup columns={2 | 3}> / <Card title href> | Linkable card grid; the blurb is the child, not a prop |
<InstallTabs target="@joinui/name" /> | shadcn add, per package manager |
<DependencyTabs packages="motion clsx" /> | npm install, per package manager |
<PackageManagerTabs commands={…} /> | Any other per-manager command |
<ComponentPreview slug title minHeight /> | Live preview |
<CodeBlock code language title /> | Highlighted code outside a fence |
<Kbd> / <Badge> | Inline key and status chips |
Callout signals severity three ways at once: an icon, a spelled-out label, and
a tint from one of the four hue families. The tint is the accelerator and the
other two are the message, so a new variant needs all three — a colour on its
own is not a variant.
Fenced blocks support a title, highlighted lines and line numbers:
```tsx title="button.tsx" {2,5-7} showLineNumbers
```Customising the AI prompt
The prompt is generated from metadata. To override it wholesale, set prompt on
the component — see using components with AI.
Before you open a pull request
pnpm checkpasses — validation, types and lint.pnpm buildsucceeds.- The component works with a keyboard alone.
- It behaves correctly with Reduce Motion enabled at the OS level.
- Both preview themes look right — check with the sun and moon buttons on the preview frame, not just by switching the site theme.
- No
dark:variants inregistry/orcomponents/previews/. - No hard-coded colours, no gradients, no shadows. Hue comes from the four
token families, and corners round through
rounded-soft*. - No
any, and noTODOleft in place of an implementation.
Publishing the registry
prebuild regenerates public/r/ before every production build, so deploying
the site publishes the registry:
pnpm buildConsumers then install from your domain:
pnpm dlx shadcn@latest add https://your-domain.com/r/glow-card.jsonSelf-hosting details are in registry setup.