shadcn/ui & Radix UI
Headless accessible components, shadcn patterns, theming, variants with cva.
reactuicomponentsfrontend
# shadcn/ui & Radix UI
## Setup shadcn/ui
```bash
npx shadcn@latest init
npx shadcn@latest add button card dialog dropdown-menu
```
## Component variants with cva
```tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
// base classes
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: { variant: 'default', size: 'default' },
}
)
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
}
```
## Radix UI primitives (headless)
```tsx
import * as Dialog from '@radix-ui/react-dialog'
import * as Select from '@radix-ui/react-select'
import * as Tooltip from '@radix-ui/react-tooltip'
// Dialog
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild><Button>Open</Button></Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[90vw] max-w-md rounded-lg bg-background p-6 shadow-xl">
<Dialog.Title>Title</Dialog.Title>
<Dialog.Description>Description</Dialog.Description>
<Dialog.Close asChild><Button variant="ghost">Close</Button></Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
```
## CSS variables theming
```css
/* globals.css */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--muted: 210 40% 96.1%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
}
/* Usage */
bg-background => hsl(var(--background))
```
## cn() utility
```ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Merges Tailwind classes correctly:
cn('px-2 py-1', 'p-4') // -> 'p-4' (not 'px-2 py-1 p-4')
```
## Compound component pattern
```tsx
const Card = ({ children, className }) => (
<div className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}>{children}</div>
)
Card.Header = ({ children }) => <div className="flex flex-col space-y-1.5 p-6">{children}</div>
Card.Title = ({ children }) => <h3 className="text-2xl font-semibold">{children}</h3>
Card.Content = ({ children }) => <div className="p-6 pt-0">{children}</div>
Card.Footer = ({ children }) => <div className="flex items-center p-6 pt-0">{children}</div>
// Usage
<Card>
<Card.Header><Card.Title>Hello</Card.Title></Card.Header>
<Card.Content>Content here</Card.Content>
</Card>
```API: /api/skills/shadcn-radix-ui