Skip to content

Contributing a Component

This guide walks through the full process of adding a new component to Prisma UI, from writing the component file to publishing its documentation and registry entry. It also covers what to do when you are only updating an existing component.

Using AI? There is a skill in .claude/skills/add-component/ that guides an AI agent through this entire workflow. Load it with any agent that supports skills.

Prisma UI lives in src/prisma/ of an Astro site. The docs are Starlight pages and the registry JSON is generated from the component sources at build time.


Follow these rules in every component:

  • Imports: Inside the library, import from @/prisma/ui/<name> and @/prisma/lib/utils. The registry rewrites them to @/components/ui/<name> and @/lib/utils for installs
  • Class merging: Always use cn() for conditional/merged class names
  • Semantic colors: Use design tokens (bg-primary, text-muted-foreground), never raw values like bg-blue-500
  • Spacing: Use flex with gap-*, never space-x-* or space-y-*
  • Variants: Use class-variance-authority (cva) when the component has multiple visual options
  • Hooks: Import directly — import { useState } from 'react', never React.useState
  • Return types: Component functions always return React.ReactNode
  • Client directive: Add "use client" if the component uses hooks, event handlers, or browser APIs
  • No framework APIs: Components install into any React project, so use plain <img> and <a>, never next/image or next/link
  • Animations: Keep keyframe animations in src/prisma/ui/style.css, not inline or in separate CSS files
  • Default animation props: When a component accepts transition or variants props (Framer Motion), provide sensible defaults so it works out of the box — users can override them, but should never be forced to pass them

Follow these steps in order. Each step builds on the previous one.

Before writing any new UI component, verify if shadcn/ui already provides it:

Terminal window
npx shadcn@latest view <component-name>
  • If shadcn has the component → use it as-is, or wrap it (see below)
  • If shadcn has something similar → extend it via a wrapper
  • If nothing exists → create from scratch following the conventions above

When extending a shadcn component, create a wrapper file instead of editing the base. For example, card-hover-effect.tsx instead of modifying card.tsx.

Add the component source at:

src/prisma/ui/<component-name>.tsx

Follow the component conventions listed above and use existing components as reference. Shared keyframes and [data-variant] properties go in src/prisma/ui/style.css, imported by the component as import './style.css'.

Create the page at:

src/content/docs/components/<component-name>.mdx

The page must follow this exact order:

  1. Frontmattertitle and description
  2. Imports — the component, ComponentPreview, Install, and any demo files
  3. Short description — one paragraph (the title is rendered from the frontmatter, no # Title)
  4. ## Installation<Install registryUrl='/components/<name>.json' />
  5. ## Import — single code block with the import statement
  6. --- separator
  7. ## Usage / variant sections — each with a <ComponentPreview> above its code block
  8. ## Props — props table at the very end
  • Every non-import code block must have a <ComponentPreview> above it showing the rendered result
  • Preview blocks should contain only the component — no extra explanation, no wrapping layout
  • When a component needs images, use Lorem Picsum with a plain <img>. Vary dimensions slightly between items for different images
  • When icons or third-party imports are needed, split them into a separate code block before the usage block

React in MDX renders as static HTML. Move a preview into its own file at src/prisma/demos/<component-name>/<n>.tsx and render it with client:visible when it:

  • is interactive (hooks, event handlers, motion, pointer effects)
  • uses compound tags such as <FlipCard.Front>, or props that take JSX such as front={<Card />} (these need the demo file even when static; leave out client:visible then)
import Demo1 from '@/prisma/demos/<component-name>/1';
<ComponentPreview>
<Demo1 client:visible />
</ComponentPreview>

4. Register the component in the docs sidebar

Section titled “4. Register the component in the docs sidebar”

Open astro.config.mjs and add the page to the right group of the Starlight sidebar:

{
label: 'Cards',
items: [
// ... existing entries
'components/<component-name>',
],
},

Add a new group if the component belongs to a new category.

Open src/prisma/registry.ts and add an entry to COMPONENTS:

{
name: '<component-name>',
dependencies: ['motion'], // every npm package the files import
registryDependencies: ['card'], // shadcn items it builds on
files: ['<component-name>.tsx', 'style.css'],
},

That is all: /components/<component-name>.json and one JSON per shadcn style are generated from the source, in development and on every build. The <Install> block on the page works as soon as the entry exists.

The sitemap and the search index pick up the new page automatically.


When you are only changing an existing component — fixing a bug, adding a variant, adjusting styles — the scope is smaller:

  1. Edit the component file at src/prisma/ui/<component-name>.tsx (and its per-style variants in src/prisma/ui/styles/ for style-aware components).
  2. Update the documentation if any props, variants, or behavior changed.
  3. Update the registry entry in src/prisma/registry.ts if the component’s dependencies or files changed. The JSON itself regenerates on its own.

Use this as a quick reference before opening a pull request.

  • Checked shadcn first
  • If shadcn had it: created a wrapper instead of modifying the base
  • Component file created at src/prisma/ui/<name>.tsx
  • "use client" present if component uses hooks, event handlers, or browser APIs
  • cn() used for all class merging
  • Semantic color tokens used (no raw bg-blue-500 etc.)
  • gap-* used for spacing (no space-x-* or space-y-*)
  • Hooks imported directly (import { useState } from 'react')
  • Return type is React.ReactNode
  • Documentation page follows correct order (frontmatter → description → Installation → Import → — → sections → Props)
  • Every non-import code block has a <ComponentPreview> above it
  • Interactive demos are demo files rendered with client:visible
  • Page added to the sidebar in astro.config.mjs
  • Entry added to COMPONENTS in src/prisma/registry.ts, with every npm import in dependencies
  • Component file updated
  • Documentation updated if API changed
  • Registry entry updated if dependencies or files changed