Skip to main content
The Drakk3 Portfolio uses shadcn/ui components for consistent, accessible UI primitives. These are React components built on top of Radix UI and styled with Tailwind CSS v4.

What is shadcn/ui?

shadcn/ui is not a component library in the traditional sense. Instead, it provides copy-and-paste components that you own and control.

Copy, not import

Components live in your codebase (src/components/ui/), not in node_modules

Fully customizable

Modify components directly without fighting wrapper APIs

Built on Radix

Accessible primitives with keyboard navigation and ARIA attributes

Styled with Tailwind

Uses utility classes and the cn() helper for composition

Available Components

The portfolio includes six shadcn/ui components:
src/components/ui/button.tsx4 variants, 4 sizes, supports asChild pattern

Button Component

Location: src/components/ui/button.tsx A versatile button component with variants and sizes, built using class-variance-authority (CVA).

Props

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}
PropTypeDefaultDescription
variant'default' | 'outline' | 'ghost' | 'link''default'Button style variant
size'default' | 'sm' | 'lg' | 'icon''default'Button size
asChildbooleanfalseRender as child element (using Radix Slot)

Full Code

src/components/ui/button.tsx
import * as React from 'react';
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-semibold transition-all disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default:
          'bg-primary text-primary-foreground hover:bg-primary/90 shadow hover:-translate-y-0.5 hover:shadow-[0_8px_24px_hsl(var(--primary)/0.35)]',
        outline:
          'border border-border bg-transparent text-foreground hover:border-primary hover:text-primary hover:-translate-y-0.5',
        ghost:
          'bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        default: 'h-10 px-6 py-2',
        sm: 'h-8 px-4 text-xs',
        lg: 'h-12 px-8 text-base',
        icon: 'h-9 w-9',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : 'button';
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    );
  }
);
Button.displayName = 'Button';

export { Button, buttonVariants };

Usage Examples

import { Button } from '@/components/ui/button';

<Button>Click me</Button>
Purple background, white text, hover lift effect.
The Contact form uses <Button type="submit" disabled={isPending}> to prevent double submissions.

Input Component

Location: src/components/ui/input.tsx Text input with focus ring, disabled states, and placeholder styling.

Props

export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
Inherits all standard HTML input props (type, placeholder, disabled, value, onChange, etc.).

Full Code

src/components/ui/input.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';

export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground',
          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
          'disabled:cursor-not-allowed disabled:opacity-50',
          'transition-colors',
          className
        )}
        ref={ref}
        {...props}
      />
    );
  }
);
Input.displayName = 'Input';

export { Input };

Usage Examples

import { Input } from '@/components/ui/input';

// Text input
<Input placeholder="Enter your name" />

// Email input
<Input type="email" placeholder="you@example.com" />

// Password input
<Input type="password" placeholder="••••••••" />

// Disabled input
<Input disabled value="Read only" />

// With state
const [value, setValue] = useState('');
<Input value={value} onChange={(e) => setValue(e.target.value)} />
The Contact form uses <Input type="email" required /> with HTML5 validation.

Textarea Component

Location: src/components/ui/textarea.tsx Multi-line text input with vertical resize.

Props

export interface TextareaProps
  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
Inherits all standard HTML textarea props (rows, cols, placeholder, disabled, etc.).

Full Code

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-[100px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground',
          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
          'disabled:cursor-not-allowed disabled:opacity-50',
          'resize-vertical transition-colors',
          className
        )}
        ref={ref}
        {...props}
      />
    );
  }
);
Textarea.displayName = 'Textarea';

export { Textarea };

Usage Examples

import { Textarea } from '@/components/ui/textarea';

// Basic
<Textarea placeholder="Enter your message" />

// Custom rows
<Textarea rows={5} />

// Disabled
<Textarea disabled value="Read only text" />

// With state
const [message, setMessage] = useState('');
<Textarea value={message} onChange={(e) => setMessage(e.target.value)} />
The Contact form uses <Textarea rows={5} required /> for the message field.

Label Component

Location: src/components/ui/label.tsx Accessible label built on Radix UI’s @radix-ui/react-label primitive.

Props

const Label = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(...);
Inherits all props from Radix Label primitive.

Full Code

src/components/ui/label.tsx
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from '@/lib/utils';

const Label = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(
      'text-sm font-medium text-muted-foreground leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
      className
    )}
    {...props}
  />
));
Label.displayName = LabelPrimitive.Root.displayName;

export { Label };

Usage Examples

import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';

// Basic
<div>
  <Label htmlFor="name">Name</Label>
  <Input id="name" />
</div>

// With required indicator
<Label htmlFor="email">
  Email <span className="text-destructive">*</span>
</Label>

// Disabled (uses peer-disabled utility)
<Label htmlFor="disabled" className="peer-disabled:opacity-50">
  Disabled Field
</Label>
<Input id="disabled" disabled className="peer" />
The Contact form uses <Label htmlFor="..."> with matching <Input id="..." /> for accessibility.

Card Component

Location: src/components/ui/card.tsx Container component with five subcomponents for structured content.

Subcomponents

export {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
  CardFooter
}

Full Code

src/components/ui/card.tsx
import * as React from 'react';
import { cn } from '@/lib/utils';

const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      className={cn(
        'rounded-lg border border-border bg-card text-card-foreground transition-all duration-250',
        className
      )}
      {...props}
    />
  )
);
Card.displayName = 'Card';

const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
  )
);
CardHeader.displayName = 'CardHeader';

const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
  ({ className, ...props }, ref) => (
    <h3
      ref={ref}
      className={cn('font-bold leading-none tracking-tight text-foreground', className)}
      {...props}
    />
  )
);
CardTitle.displayName = 'CardTitle';

const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
  ({ className, ...props }, ref) => (
    <p ref={ref} className={cn('text-sm text-muted-foreground leading-relaxed', className)} {...props} />
  )
);
CardDescription.displayName = 'CardDescription';

const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
  )
);
CardContent.displayName = 'CardContent';

const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
  )
);
CardFooter.displayName = 'CardFooter';

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

Usage Examples

import {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
  CardFooter
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';

<Card>
  <CardHeader>
    <CardTitle>Project Title</CardTitle>
    <CardDescription>A brief description of the project</CardDescription>
  </CardHeader>
  <CardContent>
    <p>Main content goes here...</p>
  </CardContent>
  <CardFooter>
    <Button>View Project</Button>
  </CardFooter>
</Card>
The Contact form wraps the form in a Card-styled <div> but doesn’t use the Card subcomponents.

Badge Component

Location: src/components/ui/badge.tsx Pill-shaped status indicator with seven variant options.

Props

export interface BadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof badgeVariants> {}
PropTypeDefaultDescription
variant'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'info''default'Badge color variant

Full Code

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 border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
  {
    variants: {
      variant: {
        default: 'border-transparent bg-primary text-primary-foreground',
        secondary: 'border-transparent bg-secondary text-secondary-foreground',
        destructive: 'border-transparent bg-destructive text-destructive-foreground',
        outline: 'border-border text-foreground',
        success: 'border-transparent bg-emerald-500/10 text-emerald-400',
        warning: 'border-transparent bg-yellow-500/10 text-yellow-400',
        info: 'border-transparent bg-indigo-500/10 text-indigo-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 };

Usage Examples

import { Badge } from '@/components/ui/badge';

<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Error</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="info">Info</Badge>

The cn() Utility

Location: src/lib/utils.ts All shadcn/ui components use the cn() helper to merge Tailwind classes.

Full Code

src/lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

What it does

  1. clsx: Conditionally joins class names
  2. twMerge: Intelligently merges Tailwind classes (last one wins)

Usage Examples

import { cn } from '@/lib/utils';

// Basic
cn('text-red-500', 'font-bold')
// → 'text-red-500 font-bold'

// Conditional
cn('base-class', isActive && 'active-class')
// → 'base-class active-class' (if isActive is true)
// → 'base-class' (if isActive is false)

// Overriding (twMerge handles conflicts)
cn('text-red-500', 'text-blue-500')
// → 'text-blue-500' (blue wins)

// Arrays and objects (via clsx)
cn(['class1', 'class2'], { 'class3': true, 'class4': false })
// → 'class1 class2 class3'

// Real example from Button component
cn(
  buttonVariants({ variant, size }),
  className
)
twMerge is essential for shadcn/ui because it prevents Tailwind class conflicts when you pass custom className props.

Tailwind Integration

All shadcn/ui components use Tailwind CSS v4 tokens defined in src/styles/global.css:
src/styles/global.css
@theme {
  --color-primary: 0 0% 100%;          /* White */
  --color-foreground: 0 0% 94%;        /* Near white */
  --color-background: 240 5% 4%;       /* Dark blue-gray */
  --color-muted-foreground: 240 5% 65%; /* Gray */
  --color-border: 240 4% 16%;          /* Dark gray */
  --color-input: 240 4% 16%;
  --color-ring: 0 0% 100%;             /* Same as primary */
}
These tokens are referenced in component classes:
'bg-primary text-primary-foreground'
'border-border bg-background text-foreground'
'text-muted-foreground'
'focus-visible:ring-ring'
bg-primary          // Purple
bg-background       // Near black
bg-card             // Slightly lighter than background
text-foreground     // Near white
text-muted-foreground // Gray
border-border       // Dark gray

Adding New shadcn/ui Components

To add more components from shadcn/ui:
1

Visit shadcn/ui docs

Go to ui.shadcn.com and find the component you want.
2

Copy the source code

Click “View Code” and copy the component code.
3

Create the file

touch src/components/ui/dialog.tsx
Paste the code and adjust imports if needed.
4

Install dependencies

Some components require Radix UI primitives:
npm install @radix-ui/react-dialog
5

Import and use

import { Dialog } from '@/components/ui/dialog';

<Dialog>...</Dialog>
Important: Always check if the component requires additional Radix UI packages. Most do.

React Components

See shadcn/ui components in action in the Contact form

Components Overview

Learn about the overall component architecture