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.
Core Technologies
Section titled “Core Technologies”Tailwind CSS v4
Section titled “Tailwind CSS v4”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:
| Token | Purpose | Usage |
|---|---|---|
background / foreground | Base page colors | Page backgrounds, main text |
card / card-foreground | Card backgrounds | Content containers |
primary / primary-foreground | Primary actions | Buttons, links, highlights |
muted / muted-foreground | Subdued content | Secondary text, disabled states |
destructive | Error/delete actions | Delete buttons, error states |
border / input / ring | UI elements | Borders, 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
Section titled “shadcn/ui”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:
cd webnpx shadcn@latest add buttonnpx shadcn@latest add cardnpx shadcn@latest add dialogAvailable 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
Section titled “motion.dev”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 animationsMotionCard- Card with hover lift effectMotionDialog- Dialog with entrance/exit animationsStatsGrid,ContentSection,ContentGrid- Dashboard wrappersAuthPageContainer,AuthCard,FormStepTransition- Auth page wrappers
Testing Note:
Animations are automatically disabled during e2e tests via the .e2e-testing class.
Inspiration Resources
Section titled “Inspiration Resources”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
Section titled “Magic UI”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 UIimport { 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
Section titled “Animata”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 Animataimport { 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
Section titled “React Bits”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
Development Workflow
Section titled “Development Workflow”Step 1: Check Existing Components
Section titled “Step 1: Check Existing Components”Before building anything new:
- Check shadcn/ui - Does it have the component? Use it.
- Check
web/src/components/ui/- Is it already installed? Use it. - Check project components - Has someone built something similar?
Step 2: Design & Inspiration
Section titled “Step 2: Design & Inspiration”If building something custom:
-
Search inspiration sites for similar patterns:
- Magic UI for animated, polished effects
- Animata for interaction animations
- React Bits for complex behaviors
-
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)?
Step 3: Implementation
Section titled “Step 3: Implementation”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> )}Step 4: Testing
Section titled “Step 4: Testing”Add data-testid attributes and test:
cd webnpx playwright test my-component.spec.ts --project=chromiumCommon Patterns
Section titled “Common Patterns”Animated List with Stagger
Section titled “Animated List with Stagger”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> )}Responsive Dialog/Sheet
Section titled “Responsive Dialog/Sheet”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> )}Loading States with Skeleton
Section titled “Loading States with Skeleton”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> )}Form with Validation
Section titled “Form with Validation”"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> )}Card with Hover Effect
Section titled “Card with Hover Effect”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> )}Customization
Section titled “Customization”Extending shadcn/ui Components
Section titled “Extending shadcn/ui Components”When you need custom variants or behavior, extend the base component:
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} /> )}Creating Custom Animations
Section titled “Creating Custom Animations”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] } }}Adapting External Components
Section titled “Adapting External Components”When copying from Magic UI, Animata, or React Bits:
- Check dependencies - Ensure they match our stack
- Adapt to motion.dev - Convert Framer Motion to motion.dev syntax (usually identical)
- Match our theme - Use our CSS variables and color tokens
- Add TypeScript - Type all props and state
- Follow our patterns - Use
cn(), add data-testid, follow file structure - 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 variantsimport { 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} /> )}Accessibility
Section titled “Accessibility”Every component must be accessible:
- ✅ Semantic HTML elements (
button,nav,main, etc.) - ✅ Keyboard navigation support
- ✅ ARIA labels for icon-only buttons
- ✅ Form labels with
htmlFormatching inputid - ✅ Focus indicators (never
outline-nonewithout custom focus styles) - ✅ Color contrast meets WCAG AA standards
- ✅ Screen reader announcements for dynamic content
- ✅ Motion respects
prefers-reduced-motion
Performance
Section titled “Performance”Best Practices
Section titled “Best Practices”- Lazy load heavy components:
const HeavyChart = dynamic(() => import("./heavy-chart"), { loading: () => <Skeleton className="h-64" />, ssr: false})- Memoize expensive renders:
const VolunteerGrid = React.memo(({ volunteers }) => { return <Grid>{volunteers.map(...)}</Grid>})- Use Server Components by default:
// No "use client" needed for static displayexport async function VolunteerList() { const volunteers = await getVolunteers() return <div>{volunteers.map(...)}</div>}- Optimize animations:
- Animate
transformandopacity(GPU-accelerated) - Avoid animating
width,height,top,left - Use
will-changesparingly
- Animate
Quick Reference
Section titled “Quick Reference”Resource Decision Matrix
Section titled “Resource Decision Matrix”| Need | Use | Resource |
|---|---|---|
| Basic UI component | shadcn/ui | npx shadcn@latest add [component] |
| Custom animation | motion.dev | web/src/lib/motion.ts variants |
| Landing page flair | Magic UI | https://magicui.design |
| Micro-interactions | Animata | https://animata.design |
| Complex patterns | React Bits | https://reactbits.dev |
| Form validation | Zod + react-hook-form | shadcn/ui form docs |
File Structure
Section titled “File Structure”web/src/components/├── ui/ # shadcn/ui components (base)├── dashboard/ # Dashboard-specific├── forms/ # Complex forms├── layout/ # Layout components└── shared/ # Shared utilitiesKey Imports
Section titled “Key Imports”// Stylingimport { cn } from "@/lib/utils"
// Animationimport { motion } from "motion/react"import { fadeVariants, slideUpVariants } from "@/lib/motion"
// Componentsimport { Button } from "@/components/ui/button"import { Card, CardHeader, CardContent } from "@/components/ui/card"
// Formsimport { useForm } from "react-hook-form"import { zodResolver } from "@hookform/resolvers/zod"import * as z from "zod"Additional Resources
Section titled “Additional Resources”- Tailwind CSS Documentation
- shadcn/ui Documentation
- motion.dev Documentation
- Radix UI Primitives
- Magic UI
- Animata
- React Bits
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.