Building UI Components
1. Component Library: shadcn/ui
- Copy-paste model: components live in
src/components/ui/, you own the code
- Built on Radix UI primitives (accessible, composable)
- Styled with Tailwind CSS + CVA (class-variance-authority) for variants
- Add components:
npx shadcn@latest add button
- Customize freely — these are YOUR components, not a dependency
2. Component Organization
| Location |
Purpose |
Examples |
src/components/ui/ |
shadcn/ui primitives |
Button, Card, Dialog, Input |
src/components/layout/ |
App shell, navigation |
Header, Sidebar, PageContainer |
src/mfes/{domain}/components/ |
Feature-specific |
ProductCard, OrderTable |
src/app/ |
Route components |
page.tsx, layout.tsx |
3. Component Pattern
// Feature component — custom props interface with readonly
interface ProductCardProps {
readonly product: Product;
readonly onDelete?: (id: string) => Promise<void>;
}
export function ProductCard({ product, onDelete }: ProductCardProps): JSX.Element {
return (
<Card>
<CardHeader>
<CardTitle>{product.name}</CardTitle>
</CardHeader>
<CardContent>{/* UI rendering */}</CardContent>
</Card>
);
}
// UI primitive — use React.ComponentProps, data-slot, export at bottom
function Card({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card"
className={cn('bg-card text-card-foreground ...', className)}
{...props}
/>
);
}
export { Card };
Rules:
- Named exports (not default, except Next.js pages)
readonly on all custom props interfaces
- Explicit return types on exported feature components
- UI primitives use
React.ComponentProps<'element'> and data-slot
- For state and logic patterns, see the managing-state skill
4. Variant System (CVA)
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 ...',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive: 'bg-destructive text-white shadow-xs hover:bg-destructive/90 ...',
outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground ...',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : 'button';
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
Key patterns:
React.ComponentProps<'element'> for prop types (not HTMLAttributes)
asChild + Slot for polymorphic rendering (render as child element)
data-slot attribute on every component for styling/testing hooks
className passed inside buttonVariants(), not as separate cn() arg
- Named exports at bottom, not inline
export function
5. Design Tokens (CSS Variables)
- Colors:
primary, secondary, destructive, muted, accent, background, foreground
- Each has a
-foreground counterpart for text
- Sidebar-specific:
sidebar-primary, sidebar-accent, etc.
- Defined in CSS, consumed via Tailwind:
bg-primary, text-muted-foreground
6. Utility: cn() Function
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Always use cn() to merge Tailwind classes — handles conflicts correctly.
7. Loading/Error/Empty States
Every data-driven component should handle three states:
function ItemsList({ items, loading, error }: ItemsListProps) {
if (loading) return <Skeleton />;
if (error !== null) return <ErrorMessage message={error.errorMessageForUser} />;
if (items.length === 0) return <EmptyState />;
return <ul>{items.map(...)}</ul>;
}
8. Accessibility Basics
- Use semantic HTML (
button not div with onClick)
- Use Radix UI primitives (already accessible)
- Always provide
aria-label for icon-only buttons
- Test with keyboard navigation
9. Cross-References
- For state management in components (stores, forms, URL state): see managing-state skill
- For API integration patterns: see building-api-clients skill
- For testing components: see testing-react-ts skill
1---2name: building-ui-components3description: UI component patterns: shadcn/ui, component organization, variant system. Use when building or modifying React UI components. ALWAYS load this skill when working with react code.4---56# Building UI Components78## 1. Component Library: shadcn/ui910- **Copy-paste model**: components live in `src/components/ui/`, you own the code11- Built on **Radix UI** primitives (accessible, composable)12- Styled with **Tailwind CSS + CVA** (class-variance-authority) for variants13- Add components: `npx shadcn@latest add button`14- Customize freely — these are YOUR components, not a dependency1516## 2. Component Organization1718| Location | Purpose | Examples |19|----------|---------|---------|20| `src/components/ui/` | shadcn/ui primitives | Button, Card, Dialog, Input |21| `src/components/layout/` | App shell, navigation | Header, Sidebar, PageContainer |22| `src/mfes/{domain}/components/` | Feature-specific | ProductCard, OrderTable |23| `src/app/` | Route components | page.tsx, layout.tsx |2425## 3. Component Pattern2627```typescript28// Feature component — custom props interface with readonly29interface ProductCardProps {30 readonly product: Product;31 readonly onDelete?: (id: string) => Promise<void>;32}3334export function ProductCard({ product, onDelete }: ProductCardProps): JSX.Element {35 return (36 <Card>37 <CardHeader>38 <CardTitle>{product.name}</CardTitle>39 </CardHeader>40 <CardContent>{/* UI rendering */}</CardContent>41 </Card>42 );43}4445// UI primitive — use React.ComponentProps, data-slot, export at bottom46function Card({ className, ...props }: React.ComponentProps<'div'>) {47 return (48 <div49 data-slot="card"50 className={cn('bg-card text-card-foreground ...', className)}51 {...props}52 />53 );54}5556export { Card };57```5859Rules:6061- **Named exports** (not default, except Next.js pages)62- **`readonly`** on all custom props interfaces63- **Explicit return types** on exported feature components64- **UI primitives** use `React.ComponentProps<'element'>` and `data-slot`65- For state and logic patterns, see the **managing-state** skill6667## 4. Variant System (CVA)6869```typescript70import { Slot } from '@radix-ui/react-slot';71import { cva, type VariantProps } from 'class-variance-authority';7273import { cn } from '@/lib/utils';7475const buttonVariants = cva(76 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 ...',77 {78 variants: {79 variant: {80 default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',81 destructive: 'bg-destructive text-white shadow-xs hover:bg-destructive/90 ...',82 outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground ...',83 secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',84 ghost: 'hover:bg-accent hover:text-accent-foreground',85 link: 'text-primary underline-offset-4 hover:underline',86 },87 size: {88 default: 'h-9 px-4 py-2 has-[>svg]:px-3',89 sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',90 lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',91 icon: 'size-9',92 },93 },94 defaultVariants: {95 variant: 'default',96 size: 'default',97 },98 }99);100101function Button({102 className,103 variant,104 size,105 asChild = false,106 ...props107}: React.ComponentProps<'button'> &108 VariantProps<typeof buttonVariants> & {109 asChild?: boolean;110 }) {111 const Comp = asChild ? Slot : 'button';112 return (113 <Comp114 data-slot="button"115 className={cn(buttonVariants({ variant, size, className }))}116 {...props}117 />118 );119}120121export { Button, buttonVariants };122```123124Key patterns:125- **`React.ComponentProps<'element'>`** for prop types (not `HTMLAttributes`)126- **`asChild` + Slot** for polymorphic rendering (render as child element)127- **`data-slot`** attribute on every component for styling/testing hooks128- **`className` passed inside `buttonVariants()`**, not as separate `cn()` arg129- **Named exports at bottom**, not inline `export function`130131## 5. Design Tokens (CSS Variables)132133- Colors: `primary`, `secondary`, `destructive`, `muted`, `accent`, `background`, `foreground`134- Each has a `-foreground` counterpart for text135- Sidebar-specific: `sidebar-primary`, `sidebar-accent`, etc.136- Defined in CSS, consumed via Tailwind: `bg-primary`, `text-muted-foreground`137138## 6. Utility: cn() Function139140```typescript141import { type ClassValue, clsx } from 'clsx';142import { twMerge } from 'tailwind-merge';143144export function cn(...inputs: ClassValue[]) {145 return twMerge(clsx(inputs));146}147```148149Always use `cn()` to merge Tailwind classes — handles conflicts correctly.150151## 7. Loading/Error/Empty States152153Every data-driven component should handle three states:154155```tsx156function ItemsList({ items, loading, error }: ItemsListProps) {157 if (loading) return <Skeleton />;158 if (error !== null) return <ErrorMessage message={error.errorMessageForUser} />;159 if (items.length === 0) return <EmptyState />;160 return <ul>{items.map(...)}</ul>;161}162```163164## 8. Accessibility Basics165166- Use semantic HTML (`button` not `div` with `onClick`)167- Use Radix UI primitives (already accessible)168- Always provide `aria-label` for icon-only buttons169- Test with keyboard navigation170171## 9. Cross-References172173- For **state management** in components (stores, forms, URL state): see **managing-state** skill174- For **API integration** patterns: see **building-api-clients** skill175- For **testing components**: see **testing-react-ts** skill