Skip to content

UI Components System

Our UI component system is built on a modern, flexible foundation that emphasizes accessibility, performance, and developer experience. This guide covers our core libraries, development patterns, and recommended resources for building exceptional user interfaces.

Tailwind CSS is our utility-first CSS framework for all styling.

Key Principles:

  • Use utility classes directly in JSX/TSX
  • Leverage the cn() helper for conditional classes
  • Follow mobile-first responsive design
  • Use CSS variables for theming

Color System:

Our colors are semantic and theme-aware through CSS variables:

TokenPurposeUsage
background / foregroundBase page colorsPage backgrounds, main text
card / card-foregroundCard backgroundsContent containers
primary / primary-foregroundPrimary actionsButtons, links, highlights
muted / muted-foregroundSubdued contentSecondary text, disabled states
destructiveError/delete actionsDelete buttons, error states
border / input / ringUI elementsBorders, form inputs, focus rings

Example Usage:

// Basic styling
<div className="flex items-center gap-4 p-6 rounded-lg bg-card">
<span className="text-foreground">Content</span>
</div>
// Responsive design
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Content */}
</div>
// Conditional classes with cn()
import { cn } from "@/lib/utils"
<div className={cn(
"px-4 py-2 rounded-md",
isActive && "bg-primary text-primary-foreground",
isDisabled && "opacity-50 cursor-not-allowed"
)}>

shadcn/ui provides our base component library. Unlike traditional component libraries, shadcn/ui components are copied into your codebase, giving you full control.

Philosophy:

  • Components are yours - copy, modify, extend
  • Built on Radix UI primitives for accessibility
  • Styled with Tailwind CSS
  • TypeScript-first

Installation Pattern:

Terminal window
cd web
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog

Available Components:

Located in web/src/components/ui/:

  • Layout: card, separator, sheet, sidebar, tabs
  • Forms: form, input, select, checkbox, radio-group, textarea, combobox
  • Feedback: alert, alert-dialog, dialog, drawer, tooltip
  • Data: table, badge, avatar, progress, skeleton
  • Navigation: dropdown-menu, command, scroll-area
  • Interactive: button, calendar, chart, switch, collapsible

Component Composition Example:

import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
export function VolunteerCard({ volunteer }) {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{volunteer.name}</CardTitle>
<Badge variant={volunteer.status === "ACTIVE" ? "default" : "secondary"}>
{volunteer.status}
</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{volunteer.email}</p>
<div className="flex gap-2 mt-4">
<Button size="sm">View Profile</Button>
<Button size="sm" variant="outline">Edit</Button>
</div>
</CardContent>
</Card>
)
}

motion.dev (evolution of Framer Motion) powers all animations in the project.

Animation Utilities:

All animation variants are centralized in web/src/lib/motion.ts:

import { motion } from "motion/react"
import {
fadeVariants,
slideUpVariants,
staggerContainer,
staggerItem
} from "@/lib/motion"
// Fade in animation
<motion.div
variants={fadeVariants}
initial="hidden"
animate="visible"
>
Content fades in
</motion.div>
// Stagger children
<motion.div variants={staggerContainer} initial="hidden" animate="visible">
{items.map(item => (
<motion.div key={item.id} variants={staggerItem}>
{item.name}
</motion.div>
))}
</motion.div>

Pre-built Motion Components:

  • MotionButton - Button with hover/tap animations
  • MotionCard - Card with hover lift effect
  • MotionDialog - Dialog with entrance/exit animations
  • StatsGrid, ContentSection, ContentGrid - Dashboard wrappers
  • AuthPageContainer, AuthCard, FormStepTransition - Auth page wrappers

Testing Note:

Animations are automatically disabled during e2e tests via the .e2e-testing class.

When building new features, these resources provide excellent patterns and inspiration. They all share our tech stack philosophy and follow the copy-paste component model.

Magic UI - 150+ animated components for landing pages and marketing sections

Best for:

  • Animated landing page components
  • Marketing sections and hero blocks
  • Eye-catching visual effects
  • Polished micro-interactions

Key Features:

  • Built with React, TypeScript, Tailwind, and Motion
  • Designed as a direct companion to shadcn/ui
  • Free open-source + Pro templates
  • Emphasis on motion and visual polish

Categories:

  • Text animations (typing effects, gradient text, reveal animations)
  • Hero sections and landing page blocks
  • Animated backgrounds (particles, grids, beams)
  • Card effects and interactions
  • Charts and data visualizations
  • Marketing-focused components

When to use:

  • Building marketing pages or public-facing sections
  • Adding visual flair to dashboards
  • Creating engaging onboarding flows
  • Enhancing user engagement with motion

Integration Example:

// Add an animated background from Magic UI
import { AnimatedGridPattern } from "@/components/ui/animated-grid-pattern"
export function HeroSection() {
return (
<div className="relative min-h-screen">
<AnimatedGridPattern className="absolute inset-0 opacity-20" />
<div className="relative z-10 flex items-center justify-center h-screen">
<h1 className="text-6xl font-bold">Welcome to Everybody Eats</h1>
</div>
</div>
)
}

Animata - 80+ hand-crafted interaction animations

Best for:

  • Micro-interactions and UI polish
  • Unique card designs
  • Creative text effects
  • Custom loading states

Key Features:

  • Hand-crafted animations curated from around the internet
  • Built with Tailwind CSS
  • Free and open-source (1000+ GitHub stars)
  • Focus on interaction quality

Categories:

  • Text Effects: Wave reveal, mirror text, typing, gradient text, gibberish
  • Cards: Shiny cards, skewed cards, GitHub-styled cards
  • Containers: Animated borders, border trails
  • Backgrounds: Animated beams, interactive grids
  • Widgets: Complex trackers, delivery status, cycling animations
  • UI Elements: Skeleton loaders, interactive components

When to use:

  • Adding delight to user interactions
  • Creating unique card designs for achievements or profiles
  • Building engaging loading states
  • Implementing creative text effects for headings

Integration Example:

// Add a shiny card effect from Animata
import { Card } from "@/components/ui/card"
import { motion } from "motion/react"
export function AchievementCard({ achievement }) {
return (
<motion.div
className="relative overflow-hidden rounded-lg"
whileHover={{ scale: 1.05 }}
>
{/* Shimmer effect */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent
-translate-x-full animate-shimmer" />
<Card className="relative">
<h3 className="text-xl font-semibold">{achievement.title}</h3>
<p className="text-muted-foreground">{achievement.description}</p>
</Card>
</motion.div>
)
}

React Bits - High-quality interactive component patterns

Best for:

  • Complete interactive components
  • Complex UI behaviors
  • Form patterns and interactions
  • Memorable user experiences

Key Features:

  • High-quality, animated, interactive React components
  • Fully customizable implementations
  • Focus on creating memorable interfaces
  • Open-source collection

When to use:

  • Building complex interactive forms
  • Implementing sophisticated UI patterns
  • Creating unique user experiences
  • Finding inspiration for component behavior

Integration:

  • Copy component patterns and adapt to our tech stack
  • Use as reference for interaction design
  • Adapt animations to motion.dev syntax

Before building anything new:

  1. Check shadcn/ui - Does it have the component? Use it.
  2. Check web/src/components/ui/ - Is it already installed? Use it.
  3. Check project components - Has someone built something similar?

If building something custom:

  1. Search inspiration sites for similar patterns:

  2. Sketch the component structure:

    • What shadcn/ui primitives can you compose?
    • What animations would enhance the UX?
    • What states does it need (loading, error, empty)?

Follow this standard pattern:

"use client" // Only if using hooks/interactivity
import { useState } from "react"
import { motion } from "motion/react"
import { cn } from "@/lib/utils"
import { Card, CardHeader, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { fadeVariants } from "@/lib/motion"
interface MyComponentProps {
className?: string
data: DataType
onAction?: () => void
}
export function MyComponent({ className, data, onAction }: MyComponentProps) {
const [isLoading, setIsLoading] = useState(false)
return (
<motion.div
variants={fadeVariants}
initial="hidden"
animate="visible"
className={cn("space-y-4", className)}
data-testid="my-component"
>
<Card>
<CardHeader>
<h2 data-testid="my-component-title">{data.title}</h2>
</CardHeader>
<CardContent>
<Button onClick={onAction} disabled={isLoading}>
{isLoading ? "Loading..." : "Take Action"}
</Button>
</CardContent>
</Card>
</motion.div>
)
}

Add data-testid attributes and test:

Terminal window
cd web
npx playwright test my-component.spec.ts --project=chromium
import { motion } from "motion/react"
import { staggerContainer, staggerItem } from "@/lib/motion"
export function VolunteerList({ volunteers }) {
return (
<motion.div
variants={staggerContainer}
initial="hidden"
animate="visible"
className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"
>
{volunteers.map(volunteer => (
<motion.div key={volunteer.id} variants={staggerItem}>
<VolunteerCard volunteer={volunteer} />
</motion.div>
))}
</motion.div>
)
}

Use the ResponsiveDialog component (desktop dialog, mobile sheet):

import { ResponsiveDialog } from "@/components/ui/responsive-dialog"
export function EditVolunteerDialog({ volunteer, open, onOpenChange }) {
return (
<ResponsiveDialog
open={open}
onOpenChange={onOpenChange}
title="Edit Volunteer"
description="Update volunteer information"
>
<VolunteerForm volunteer={volunteer} />
</ResponsiveDialog>
)
}
import { Skeleton } from "@/components/ui/skeleton"
export function DashboardStats({ isLoading, stats }) {
if (isLoading) {
return (
<div className="grid gap-4 md:grid-cols-3">
{[1, 2, 3].map(i => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
)
}
return (
<div className="grid gap-4 md:grid-cols-3">
{stats.map(stat => (
<StatsCard key={stat.id} stat={stat} />
))}
</div>
)
}
"use client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
const formSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
})
export function VolunteerForm({ onSubmit }) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { name: "", email: "" },
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
import { MotionCard } from "@/components/ui/motion-card"
import { Badge } from "@/components/ui/badge"
export function ShiftCard({ shift }) {
return (
<MotionCard className="cursor-pointer">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold">{shift.title}</h3>
<Badge variant={shift.status === "OPEN" ? "default" : "secondary"}>
{shift.status}
</Badge>
</div>
<p className="text-sm text-muted-foreground">{shift.description}</p>
<div className="flex gap-2 text-xs text-muted-foreground">
<span>{shift.date}</span>
<span></span>
<span>{shift.duration} hours</span>
</div>
</div>
</MotionCard>
)
}

When you need custom variants or behavior, extend the base component:

web/src/components/ui/gradient-button.tsx
import { Button, ButtonProps } from "@/components/ui/button"
import { cn } from "@/lib/utils"
export function GradientButton({ className, ...props }: ButtonProps) {
return (
<Button
className={cn(
"bg-gradient-to-r from-primary to-purple-600",
"hover:from-primary/90 hover:to-purple-600/90",
className
)}
{...props}
/>
)
}

Add new animation variants to web/src/lib/motion.ts:

export const customVariants = {
hidden: {
opacity: 0,
y: 20,
scale: 0.95
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.5,
ease: [0.4, 0, 0.2, 1]
}
}
}

When copying from Magic UI, Animata, or React Bits:

  1. Check dependencies - Ensure they match our stack
  2. Adapt to motion.dev - Convert Framer Motion to motion.dev syntax (usually identical)
  3. Match our theme - Use our CSS variables and color tokens
  4. Add TypeScript - Type all props and state
  5. Follow our patterns - Use cn(), add data-testid, follow file structure
  6. Test thoroughly - Ensure it works across screen sizes

Example Adaptation:

// From Magic UI (Framer Motion)
import { motion } from "framer-motion"
// Adapt to our project (motion.dev)
import { motion } from "motion/react"
import { fadeVariants } from "@/lib/motion" // Use our variants
import { cn } from "@/lib/utils" // Use our utilities
export function AdaptedComponent({ className, ...props }: AdaptedComponentProps) {
return (
<motion.div
variants={fadeVariants}
className={cn("base-styles", className)}
data-testid="adapted-component"
{...props}
/>
)
}

Every component must be accessible:

  • ✅ Semantic HTML elements (button, nav, main, etc.)
  • ✅ Keyboard navigation support
  • ✅ ARIA labels for icon-only buttons
  • ✅ Form labels with htmlFor matching input id
  • ✅ Focus indicators (never outline-none without custom focus styles)
  • ✅ Color contrast meets WCAG AA standards
  • ✅ Screen reader announcements for dynamic content
  • ✅ Motion respects prefers-reduced-motion
  1. Lazy load heavy components:
const HeavyChart = dynamic(() => import("./heavy-chart"), {
loading: () => <Skeleton className="h-64" />,
ssr: false
})
  1. Memoize expensive renders:
const VolunteerGrid = React.memo(({ volunteers }) => {
return <Grid>{volunteers.map(...)}</Grid>
})
  1. Use Server Components by default:
// No "use client" needed for static display
export async function VolunteerList() {
const volunteers = await getVolunteers()
return <div>{volunteers.map(...)}</div>
}
  1. Optimize animations:
    • Animate transform and opacity (GPU-accelerated)
    • Avoid animating width, height, top, left
    • Use will-change sparingly
NeedUseResource
Basic UI componentshadcn/uinpx shadcn@latest add [component]
Custom animationmotion.devweb/src/lib/motion.ts variants
Landing page flairMagic UIhttps://magicui.design
Micro-interactionsAnimatahttps://animata.design
Complex patternsReact Bitshttps://reactbits.dev
Form validationZod + react-hook-formshadcn/ui form docs
web/src/components/
├── ui/ # shadcn/ui components (base)
├── dashboard/ # Dashboard-specific
├── forms/ # Complex forms
├── layout/ # Layout components
└── shared/ # Shared utilities
// Styling
import { cn } from "@/lib/utils"
// Animation
import { motion } from "motion/react"
import { fadeVariants, slideUpVariants } from "@/lib/motion"
// Components
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardContent } from "@/components/ui/card"
// Forms
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"

Remember: Start with shadcn/ui, enhance with Tailwind, animate with motion.dev, and find inspiration from Magic UI, Animata, and React Bits. Build components that are accessible, performant, and delightful to use.