shadcn/ui Components
shadcn/ui is a collection of beautifully designed, accessible components. Unlike traditional component libraries, you copy the source code into your project and own it completely.
Why shadcn/ui?
- No dependency - Components live in your codebase
- Customizable - Modify anything you want
- Accessible - Built on Radix UI primitives
- Beautiful - Modern, polished designs
Installing Components
You can use the shadcn CLI, but for our app we'll create the components manually to understand them better.
Components We Need
| Component | Purpose |
|---|---|
| Button | Actions, submit |
| Textarea | Chat input |
| Dialog | Preferences modal |
| Tabs | Preference categories |
| Badge | Preference tags |
| Card | Container styling |
Step 1: Button
Create src/components/ui/button.tsx:
src/components/ui/button.tsx
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-orange-500 text-white hover:bg-orange-600',
ghost: 'hover:bg-slate-800 hover:text-white',
outline: 'border border-slate-700 hover:bg-slate-800',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-8 px-3 text-sm',
lg: 'h-12 px-6 text-lg',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };
Understanding CVA
Class Variance Authority (cva) creates type-safe variant classes:
// Usage
<Button variant="ghost" size="sm">Click me</Button>
// Renders with: hover:bg-slate-800 h-8 px-3 text-sm
Step 2: Textarea
Create src/components/ui/textarea.tsx:
src/components/ui/textarea.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[60px] w-full rounded-xl border border-slate-700 bg-slate-800/50 px-4 py-3 text-sm text-white placeholder:text-slate-500 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
);
Textarea.displayName = 'Textarea';
export { Textarea };
Step 3: Dialog (Modal)
Create src/components/ui/dialog.tsx:
src/components/ui/dialog.tsx
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { cn } from '@/lib/utils';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 backdrop-blur-sm',
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] rounded-xl border border-slate-800 bg-slate-900 p-6 shadow-xl',
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-2 mb-4', className)} {...props} />
);
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-white', className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogClose,
};
You'll need to install Radix Dialog:
bun add @radix-ui/react-dialog
Step 4: Badge
Create src/components/ui/badge.tsx:
src/components/ui/badge.tsx
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const badgeVariants = cva(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors',
{
variants: {
variant: {
default: 'bg-orange-500/20 text-orange-400',
secondary: 'bg-slate-700 text-slate-300',
outline: 'border border-slate-700 text-slate-400',
},
},
defaultVariants: {
variant: 'default',
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
Step 5: Tabs
Create src/components/ui/tabs.tsx:
src/components/ui/tabs.tsx
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '@/lib/utils';
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-lg bg-slate-800/50 p-1',
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium text-slate-400 transition-all hover:text-white data-[state=active]:bg-slate-700 data-[state=active]:text-white',
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn('mt-4', className)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
Install Radix Tabs:
bun add @radix-ui/react-tabs
Component Index
Create src/components/ui/index.ts:
src/components/ui/index.ts
export { Button } from './button';
export { Textarea } from './textarea';
export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogClose } from './dialog';
export { Badge } from './badge';
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
Import Pattern
Now you can import like this:
import { Button, Textarea, Dialog } from '@/components/ui';
// Or individually
import { Button } from '@/components/ui/button';
Next, let's build the chat interface components.