Skip to main content

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

ComponentPurpose
ButtonActions, submit
TextareaChat input
DialogPreferences modal
TabsPreference categories
BadgePreference tags
CardContainer 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.