src/components/ui/— low-level primitives (Button, Card, Input)src/components/— composite components that combine primitives (Layout, Navigation)
shadcn/ui CLI
Add a ready-made, accessible primitive from the shadcn/ui registry in one command.
Custom primitive
Write a new low-level component in
src/components/ui/ following the existing pattern.Composite component
Combine multiple primitives into a larger component in
src/components/.Option A: Add a shadcn/ui component via CLI
The project is pre-configured with acomponents.json that points the shadcn/ui CLI at the right directories:
components.json
badge component:
src/components/ui/badge.tsx and installs any required dependencies. Import it using the @/ alias:
Option B: Create a custom primitive
Follow the same structure as the existingButton component when building a new low-level primitive.
The cn() utility
All components use cn() from @/lib/utils to merge Tailwind classes safely. It combines clsx and tailwind-merge so conflicting utility classes resolve correctly:
className prop so callers can extend styles without forking the component.
Simple custom primitive
Here is aBadge primitive written from scratch following the project conventions:
src/components/ui/badge.tsx
- Use
cva()fromclass-variance-authorityto define variants - Extend the relevant HTML element’s props interface (e.g.,
React.HTMLAttributes<HTMLSpanElement>) - Add
VariantProps<typeof yourVariants>to expose variant props with full type safety - Apply
data-slot="component-name"for consistent targeting in tests and CSS - Spread
...propsafter your own props so all native HTML attributes pass through - Export both the component and the variants function
Using variants in a component
Once the component is written, import and use it like any other primitive:Option C: Create a composite component
Composite components live insrc/components/ (not src/components/ui/). They import and combine primitives to build higher-level UI.
Here is an example FeatureCard that composes Card, CardHeader, CardTitle, and CardContent:
src/components/FeatureCard.tsx
Composite components use the
@/components/ui/* import path, not relative paths. This keeps imports consistent even if you move files later.Accepting and merging className props
Every component should forward a className prop so callers can override or extend styles:
cn() here is important. Without it, passing className would concatenate strings, and conflicting Tailwind utilities (like two text- values) would both appear in the output — only the last one would win based on CSS specificity, not the order you specified.