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:
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 ;
}
Prop Type Default Description 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
Default
Outline
Ghost
Link
Sizes
asChild Pattern
Disabled
import { Button } from '@/components/ui/button' ;
< Button > Click me </ Button >
Purple background, white text, hover lift effect. < Button variant = "outline" > Click me </ Button >
Transparent background, border, hover changes border color. < Button variant = "ghost" > Click me </ Button >
Transparent, no border, hover adds background. < Button variant = "link" > Click me </ Button >
Styled like a link with underline on hover. < Button size = "sm" > Small </ Button >
< Button size = "default" > Default </ Button >
< Button size = "lg" > Large </ Button >
< Button size = "icon" > 🔥 </ Button >
< Button asChild >
< a href = "/projects" > View Projects </ a >
</ Button >
Renders an <a> tag with Button styles. Uses Radix UI’s Slot component. < Button disabled > Can't click me </ Button >
Reduced opacity, pointer events disabled.
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 > {}
Prop Type Default Description 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
import { type ClassValue , clsx } from 'clsx' ;
import { twMerge } from 'tailwind-merge' ;
export function cn ( ... inputs : ClassValue []) {
return twMerge ( clsx ( inputs ));
}
What it does
clsx : Conditionally joins class names
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:
@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'
Color Tokens
Focus Ring
Disabled State
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
focus - visible : outline - none
focus - visible : ring - 2
focus - visible : ring - ring
focus - visible : ring - offset - 2
Used by Input, Textarea, and Button. disabled : pointer - events - none
disabled : opacity - 50
disabled : cursor - not - allowed
Used by all form components.
Adding New shadcn/ui Components
To add more components from shadcn/ui:
Copy the source code
Click “View Code” and copy the component code.
Create the file
touch src/components/ui/dialog.tsx
Paste the code and adjust imports if needed.
Install dependencies
Some components require Radix UI primitives: npm install @radix-ui/react-dialog
Import and use
import { Dialog } from '@/components/ui/dialog' ;
< Dialog > ... </ Dialog >
Important: Always check if the component requires additional Radix UI packages. Most do.
Related Pages
React Components See shadcn/ui components in action in the Contact form
Components Overview Learn about the overall component architecture