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.