Frontend Development Prompt Templates

AI prompt templates for frontend development. Build responsive UIs with React, Vue, and more.

Overview

Frontend development creates the user-facing layer of applications, what users see and interact with. These prompts help you build components, handle state, implement responsive designs, and solve common UI challenges. The key is specifying your framework, design requirements, and interaction patterns for code that fits your project.

Best Practices

1

Specify your framework and version (React 18, Vue 3, etc.), syntax and patterns differ significantly

2

Include your styling approach (Tailwind, CSS modules, styled-components) for consistent output

3

Describe the component's purpose and where it fits in the UI hierarchy

4

Mention accessibility requirements, they affect component structure and attributes

5

Include any design system or component library you're using (shadcn/ui, MUI, etc.)

Prompt Templates

1. React Component Generator

Create a React component with these specifications:

**Component name:** [NAME]
**Purpose:** [WHAT IT DOES]

**Props:**
- [PROP 1]: [TYPE] - [DESCRIPTION]
- [PROP 2]: [TYPE] - [DESCRIPTION]

**Features:**
- [FEATURE 1 - e.g. loading state]
- [FEATURE 2 - e.g. error handling]
- [FEATURE 3 - e.g. pagination]

**Styling:**
- Method: [TAILWIND / CSS MODULES / STYLED-COMPONENTS]
- Design system: [IF ANY]

**State management:**
- Local state: [WHAT STATE IT MANAGES]
- External: [REDUX / CONTEXT / ZUSTAND / NONE]

**Accessibility:**
- [REQUIREMENTS - e.g. keyboard navigation, screen reader support]

**Provide:**
1. Component code with TypeScript
2. Props interface
3. Usage example
4. Any custom hooks if needed
Component: UserCard
Purpose: Display user profile info with actions
Props: user (User object), onEdit (callback), onDelete (callback), isLoading (boolean)
Features: Loading skeleton, confirmation modal for delete, responsive layout
Styling: Tailwind CSS
State: Modal open/close
Accessibility: Keyboard accessible buttons, proper ARIA labels
```typescript
// UserCard.tsx
import { useState } from 'react';
import { User } from '@/types';
import { ConfirmModal } from '@/components/ConfirmModal';
import { Skeleton } from '@/components/Skeleton';

interface UserCardProps {
 user: User;
 onEdit: (userId: string) => void;
 onDelete: (userId: string) => Promise<void>;
 isLoading?: boolean;
}

export function UserCard({ user, onEdit, onDelete, isLoading = false }: UserCardProps) {
 const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
 const [isDeleting, setIsDeleting] = useState(false);

 const handleDelete = async () => {
 setIsDeleting(true);
 try {
 await onDelete(user.id);
 setIsDeleteModalOpen(false);
 } finally {
 setIsDeleting(false);
 }
 };

 if (isLoading) {
 return <UserCardSkeleton />;
 }

 return (
 <>
 <article
 className="bg-white rounded-lg shadow-md p-4 sm:p-6 flex flex-col sm:flex-row gap-4"
 aria-label={`Profile card for ${user.name}`}
 >
 {/* Avatar */}
 <div className="flex-shrink-0">
 <img
 src={user.avatarUrl || '/default-avatar.png'}
 alt=""
 className="w-16 h-16 sm:w-20 sm:h-20 rounded-full object-cover"
 aria-hidden="true"
 />
 </div>

 {/* User Info */}
 <div className="flex-grow min-w-0">
 <h3 className="text-lg font-semibold text-gray-900 truncate">
 {user.name}
 </h3>
 <p className="text-sm text-gray-500 truncate">{user.email}</p>
 {user.role && (
 <span className="inline-block mt-2 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded">
 {user.role}
 </span>
 )}
 </div>

 {/* Actions */}
 <div className="flex sm:flex-col gap-2 sm:ml-4">
 <button
 onClick={() => onEdit(user.id)}
 className="flex-1 sm:flex-none px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 rounded-md hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
 aria-label={`Edit ${user.name}'s profile`}
 >
 Edit
 </button>
 <button
 onClick={() => setIsDeleteModalOpen(true)}
 className="flex-1 sm:flex-none px-4 py-2 text-sm font-medium text-red-600 bg-red-50 rounded-md hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-colors"
 aria-label={`Delete ${user.name}'s account`}
 >
 Delete
 </button>
 </div>
 </article>

 {/* Delete Confirmation Modal */}
 <ConfirmModal
 isOpen={isDeleteModalOpen}
 onClose={() => setIsDeleteModalOpen(false)}
 onConfirm={handleDelete}
 isLoading={isDeleting}
 title="Delete User"
 message={`Are you sure you want to delete ${user.name}? This action cannot be undone.`}
 confirmText="Delete"
 confirmVariant="danger"
 />
 </>
 );
}

// Loading skeleton
function UserCardSkeleton() {
 return (
 <div className="bg-white rounded-lg shadow-md p-4 sm:p-6 flex flex-col sm:flex-row gap-4 animate-pulse">
 <Skeleton className="w-16 h-16 sm:w-20 sm:h-20 rounded-full" />
 <div className="flex-grow space-y-2">
 <Skeleton className="h-5 w-32" />
 <Skeleton className="h-4 w-48" />
 <Skeleton className="h-6 w-16 mt-2" />
 </div>
 <div className="flex sm:flex-col gap-2">
 <Skeleton className="h-9 w-20" />
 <Skeleton className="h-9 w-20" />
 </div>
 </div>
 );
}
```

### Usage Example

```typescript
import { UserCard } from '@/components/UserCard';
import { useUser } from '@/hooks/useUser';

function UserProfile({ userId }: { userId: string }) {
 const { user, isLoading, deleteUser } = useUser(userId);
 const router = useRouter();

 return (
 <UserCard
 user={user}
 isLoading={isLoading}
 onEdit={(id) => router.push(`/users/${id}/edit`)}
 onDelete={deleteUser}
 />
 );
}
```

When building new React components, refactoring existing components, or learning component patterns

  • Separate loading states into skeleton components for better UX and code organization
  • Use aria-label for buttons with actions, screen readers need context beyond just 'Delete'

2. Form Implementation

Create a form component with validation:

**Form purpose:** [WHAT DATA IT COLLECTS]
**Framework:** [REACT / VUE / ANGULAR]

**Fields:**
- [FIELD 1]: [TYPE] - [VALIDATION RULES]
- [FIELD 2]: [TYPE] - [VALIDATION RULES]
- [FIELD 3]: [TYPE] - [VALIDATION RULES]

**Form library:** [REACT-HOOK-FORM / FORMIK / VEE-VALIDATE / NATIVE]
**Validation:** [ZOD / YUP / CUSTOM]

**Behavior:**
- Submit action: [WHAT HAPPENS ON SUBMIT]
- Error handling: [HOW TO SHOW ERRORS]
- Loading state: [WHILE SUBMITTING]

**Styling:** [TAILWIND / CSS / COMPONENT LIBRARY]

**Provide:**
1. Form component
2. Validation schema
3. Error display
4. Submit handling
Purpose: User registration form
Framework: React
Fields: name (required, 2-50 chars), email (required, valid email), password (required, 8+ chars, 1 number), confirmPassword (must match)
Library: React Hook Form + Zod
Behavior: Submit to API, show field-level errors, disable button while submitting
Styling: Tailwind CSS
```typescript
// RegistrationForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useState } from 'react';

// Validation schema
const registrationSchema = z
.object({
 name: z
.string()
.min(2, 'Name must be at least 2 characters')
.max(50, 'Name must be less than 50 characters'),
 email: z
.string()
.email('Please enter a valid email address'),
 password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/\d/, 'Password must contain at least one number'),
 confirmPassword: z.string(),
 })
.refine((data) => data.password === data.confirmPassword, {
 message: 'Passwords do not match',
 path: ['confirmPassword'],
 });

type RegistrationFormData = z.infer<typeof registrationSchema>;

interface RegistrationFormProps {
 onSubmit: (data: Omit<RegistrationFormData, 'confirmPassword'>) => Promise<void>;
}

export function RegistrationForm({ onSubmit }: RegistrationFormProps) {
 const [submitError, setSubmitError] = useState<string | null>(null);

 const {
 register,
 handleSubmit,
 formState: { errors, isSubmitting },
 } = useForm<RegistrationFormData>({
 resolver: zodResolver(registrationSchema),
 mode: 'onBlur', // Validate on blur for better UX
 });

 const handleFormSubmit = async (data: RegistrationFormData) => {
 setSubmitError(null);
 try {
 const { confirmPassword...submitData } = data;
 await onSubmit(submitData);
 } catch (error) {
 setSubmitError(
 error instanceof Error ? error.message : 'Registration failed. Please try again.'
 );
 }
 };

 return (
 <form
 onSubmit={handleSubmit(handleFormSubmit)}
 className="space-y-6 max-w-md mx-auto"
 noValidate
 >
 {/* Global error */}
 {submitError && (
 <div
 className="p-4 bg-red-50 border border-red-200 rounded-md text-red-700 text-sm"
 role="alert"
 >
 {submitError}
 </div>
 )}

 {/* Name field */}
 <div>
 <label htmlFor="name" className="block text-sm font-medium text-gray-700">
 Full Name
 </label>
 <input
 id="name"
 type="text"
 autoComplete="name"
 {...register('name')}
 className={`mt-1 block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
 errors.name ? 'border-red-300' : 'border-gray-300'
 }`}
 aria-invalid={errors.name ? 'true' : 'false'}
 aria-describedby={errors.name ? 'name-error' : undefined}
 />
 {errors.name && (
 <p id="name-error" className="mt-1 text-sm text-red-600" role="alert">
 {errors.name.message}
 </p>
 )}
 </div>

 {/* Email field */}
 <div>
 <label htmlFor="email" className="block text-sm font-medium text-gray-700">
 Email Address
 </label>
 <input
 id="email"
 type="email"
 autoComplete="email"
 {...register('email')}
 className={`mt-1 block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
 errors.email ? 'border-red-300' : 'border-gray-300'
 }`}
 aria-invalid={errors.email ? 'true' : 'false'}
 aria-describedby={errors.email ? 'email-error' : undefined}
 />
 {errors.email && (
 <p id="email-error" className="mt-1 text-sm text-red-600" role="alert">
 {errors.email.message}
 </p>
 )}
 </div>

 {/* Password field */}
 <div>
 <label htmlFor="password" className="block text-sm font-medium text-gray-700">
 Password
 </label>
 <input
 id="password"
 type="password"
 autoComplete="new-password"
 {...register('password')}
 className={`mt-1 block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
 errors.password ? 'border-red-300' : 'border-gray-300'
 }`}
 aria-invalid={errors.password ? 'true' : 'false'}
 aria-describedby={errors.password ? 'password-error' : 'password-hint'}
 />
 <p id="password-hint" className="mt-1 text-xs text-gray-500">
 At least 8 characters with 1 number
 </p>
 {errors.password && (
 <p id="password-error" className="mt-1 text-sm text-red-600" role="alert">
 {errors.password.message}
 </p>
 )}
 </div>

 {/* Confirm Password field */}
 <div>
 <label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">
 Confirm Password
 </label>
 <input
 id="confirmPassword"
 type="password"
 autoComplete="new-password"
 {...register('confirmPassword')}
 className={`mt-1 block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
 errors.confirmPassword ? 'border-red-300' : 'border-gray-300'
 }`}
 aria-invalid={errors.confirmPassword ? 'true' : 'false'}
 aria-describedby={errors.confirmPassword ? 'confirm-error' : undefined}
 />
 {errors.confirmPassword && (
 <p id="confirm-error" className="mt-1 text-sm text-red-600" role="alert">
 {errors.confirmPassword.message}
 </p>
 )}
 </div>

 {/* Submit button */}
 <button
 type="submit"
 disabled={isSubmitting}
 className="w-full py-2 px-4 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
 >
 {isSubmitting ? (
 <span className="flex items-center justify-center gap-2">
 <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
 <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
 <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
 </svg>
 Creating account...
 </span>
 ) : (
 'Create Account'
 )}
 </button>
 </form>
 );
}
```

### Usage

```typescript
import { RegistrationForm } from '@/components/RegistrationForm';
import { useRouter } from 'next/navigation';

export default function RegisterPage() {
 const router = useRouter();

 const handleRegister = async (data: { name: string; email: string; password: string }) => {
 const response = await fetch('/api/register', {
 method: 'POST',
 headers: { 'Content-Type': 'application/json' },
 body: JSON.stringify(data),
 });

 if (!response.ok) {
 const error = await response.json();
 throw new Error(error.message || 'Registration failed');
 }

 router.push('/login?registered=true');
 };

 return (
 <div className="min-h-screen flex items-center justify-center py-12 px-4">
 <div className="w-full max-w-md">
 <h1 className="text-2xl font-bold text-center mb-8">Create Your Account</h1>
 <RegistrationForm onSubmit={handleRegister} />
 </div>
 </div>
 );
}
```

When building forms with validation, user input collection, or data entry interfaces

  • Use mode: 'onBlur' for better UX, validating on every keystroke is annoying
  • Always include aria-invalid and aria-describedby for accessibility

3. Data Fetching Component

Create a component that fetches and displays data:

**Data type:** [WHAT DATA]
**Framework:** [REACT / VUE / etc.]
**Fetching method:** [REACT QUERY / SWR / USE EFFECT / SERVER COMPONENTS]

**Features:**
- Loading state: [HOW TO SHOW LOADING]
- Error state: [HOW TO SHOW ERRORS]
- Empty state: [WHEN NO DATA]
- Pagination/Infinite scroll: [IF NEEDED]
- Caching: [REQUIREMENTS]

**API endpoint:** [ENDPOINT STRUCTURE]

**Provide:**
1. Component code
2. Custom hook for data fetching
3. Loading/error/empty states
4. TypeScript types
Data: List of blog posts
Framework: React with Next.js 14 App Router
Fetching: React Query (TanStack Query)
Features: Loading skeleton, error with retry, empty state, infinite scroll
API: GET /api/posts?cursor=xxx&limit=10
Caching: 5 minute stale time
```typescript
// hooks/usePosts.ts
import { useInfiniteQuery } from '@tanstack/react-query';

interface Post {
 id: string;
 title: string;
 excerpt: string;
 author: { name: string; avatarUrl: string };
 publishedAt: string;
 readingTime: number;
}

interface PostsResponse {
 posts: Post[];
 nextCursor: string | null;
 hasMore: boolean;
}

async function fetchPosts(cursor?: string): Promise<PostsResponse> {
 const params = new URLSearchParams({ limit: '10' });
 if (cursor) params.set('cursor', cursor);

 const response = await fetch(`/api/posts?${params}`);
 if (!response.ok) {
 throw new Error('Failed to fetch posts');
 }
 return response.json();
}

export function usePosts() {
 return useInfiniteQuery({
 queryKey: ['posts'],
 queryFn: ({ pageParam }) => fetchPosts(pageParam),
 initialPageParam: undefined as string | undefined,
 getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
 staleTime: 5 * 60 * 1000, // 5 minutes
 });
}
```

```typescript
// components/PostList.tsx
'use client';

import { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';
import { usePosts } from '@/hooks/usePosts';
import { PostCard, PostCardSkeleton } from './PostCard';

export function PostList() {
 const {
 data,
 fetchNextPage,
 hasNextPage,
 isFetchingNextPage,
 isLoading,
 isError,
 error,
 refetch,
 } = usePosts();

 // Infinite scroll trigger
 const { ref: loadMoreRef, inView } = useInView({
 threshold: 0,
 rootMargin: '100px',
 });

 useEffect(() => {
 if (inView && hasNextPage && !isFetchingNextPage) {
 fetchNextPage();
 }
 }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);

 // Loading state
 if (isLoading) {
 return (
 <div className="space-y-6">
 {Array.from({ length: 5 }).map((_, i) => (
 <PostCardSkeleton key={i} />
 ))}
 </div>
 );
 }

 // Error state
 if (isError) {
 return (
 <div className="text-center py-12">
 <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 mb-4">
 <svg className="w-8 h-8 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
 </svg>
 </div>
 <h3 className="text-lg font-medium text-gray-900 mb-2">Failed to load posts</h3>
 <p className="text-gray-500 mb-4">{error?.message || 'Something went wrong'}</p>
 <button
 onClick={() => refetch()}
 className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
 >
 Try again
 </button>
 </div>
 );
 }

 // Flatten pages into single array
 const posts = data?.pages.flatMap((page) => page.posts) ?? [];

 // Empty state
 if (posts.length === 0) {
 return (
 <div className="text-center py-12">
 <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 mb-4">
 <svg className="w-8 h-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
 </svg>
 </div>
 <h3 className="text-lg font-medium text-gray-900 mb-2">No posts yet</h3>
 <p className="text-gray-500">Check back later for new content.</p>
 </div>
 );
 }

 return (
 <div className="space-y-6">
 {posts.map((post) => (
 <PostCard key={post.id} post={post} />
 ))}

 {/* Load more trigger */}
 {hasNextPage && (
 <div ref={loadMoreRef} className="py-4">
 {isFetchingNextPage && (
 <div className="space-y-6">
 <PostCardSkeleton />
 <PostCardSkeleton />
 </div>
 )}
 </div>
 )}

 {/* End of list */}
 {!hasNextPage && posts.length > 0 && (
 <p className="text-center text-gray-500 py-4">
 You've reached the end
 </p>
 )}
 </div>
 );
}
```

```typescript
// components/PostCard.tsx
import Link from 'next/link';
import { formatDistanceToNow } from 'date-fns';

interface Post {
 id: string;
 title: string;
 excerpt: string;
 author: { name: string; avatarUrl: string };
 publishedAt: string;
 readingTime: number;
}

export function PostCard({ post }: { post: Post }) {
 return (
 <article className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
 <Link href={`/posts/${post.id}`} className="block">
 <h2 className="text-xl font-semibold text-gray-900 mb-2 hover:text-blue-600">
 {post.title}
 </h2>
 <p className="text-gray-600 mb-4 line-clamp-2">{post.excerpt}</p>
 </Link>
 
 <div className="flex items-center justify-between text-sm text-gray-500">
 <div className="flex items-center gap-2">
 <img
 src={post.author.avatarUrl}
 alt={post.author.name}
 className="w-6 h-6 rounded-full"
 />
 <span>{post.author.name}</span>
 </div>
 <div className="flex items-center gap-4">
 <span>{formatDistanceToNow(new Date(post.publishedAt))} ago</span>
 <span>{post.readingTime} min read</span>
 </div>
 </div>
 </article>
 );
}

export function PostCardSkeleton() {
 return (
 <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 animate-pulse">
 <div className="h-6 bg-gray-200 rounded w-3/4 mb-2" />
 <div className="h-4 bg-gray-200 rounded w-full mb-1" />
 <div className="h-4 bg-gray-200 rounded w-2/3 mb-4" />
 <div className="flex justify-between">
 <div className="flex items-center gap-2">
 <div className="w-6 h-6 bg-gray-200 rounded-full" />
 <div className="h-4 bg-gray-200 rounded w-24" />
 </div>
 <div className="h-4 bg-gray-200 rounded w-32" />
 </div>
 </div>
 );
}
```

When building components that fetch and display data from APIs with proper loading and error states

  • Use Intersection Observer for infinite scroll, more efficient than scroll event listeners
  • Always flatten paginated data before rendering to handle page boundaries correctly

4. Responsive Layout

Create a responsive layout component:

**Layout type:** [DASHBOARD / LANDING PAGE / APP SHELL / etc.]
**Framework:** [REACT / VUE / etc.]

**Breakpoints:**
- Mobile: [BEHAVIOR]
- Tablet: [BEHAVIOR]
- Desktop: [BEHAVIOR]

**Sections:**
- [SECTION 1 - e.g. navigation]
- [SECTION 2 - e.g. sidebar]
- [SECTION 3 - e.g. main content]

**Features:**
- [FEATURE 1 - e.g. collapsible sidebar]
- [FEATURE 2 - e.g. mobile menu]

**Styling:** [TAILWIND / CSS GRID / FLEXBOX]

**Provide:**
1. Layout component
2. Responsive behavior
3. Navigation components
4. Mobile menu implementation
Layout: Dashboard with sidebar
Framework: React
Breakpoints: Mobile (stacked, hamburger menu), Tablet (collapsed sidebar icons), Desktop (full sidebar)
Sections: Header, Sidebar navigation, Main content area
Features: Collapsible sidebar, mobile drawer, active route highlighting
Styling: Tailwind CSS
```typescript
// components/DashboardLayout.tsx
'use client';

import { useState, createContext, useContext } from 'react';
import { usePathname } from 'next/navigation';
import Link from 'next/link';

// Sidebar context for collapse state
const SidebarContext = createContext<{
 isCollapsed: boolean;
 isMobileOpen: boolean;
 toggleCollapse: () => void;
 toggleMobile: () => void;
}>(null!);

export function useSidebar() {
 return useContext(SidebarContext);
}

interface DashboardLayoutProps {
 children: React.ReactNode;
}

export function DashboardLayout({ children }: DashboardLayoutProps) {
 const [isCollapsed, setIsCollapsed] = useState(false);
 const [isMobileOpen, setIsMobileOpen] = useState(false);

 return (
 <SidebarContext.Provider
 value={{
 isCollapsed,
 isMobileOpen,
 toggleCollapse: () => setIsCollapsed(!isCollapsed),
 toggleMobile: () => setIsMobileOpen(!isMobileOpen),
 }}
 >
 <div className="min-h-screen bg-gray-50">
 {/* Mobile overlay */}
 {isMobileOpen && (
 <div
 className="fixed inset-0 bg-black/50 z-40 lg:hidden"
 onClick={() => setIsMobileOpen(false)}
 />
 )}

 {/* Sidebar */}
 <Sidebar />

 {/* Main content */}
 <div
 className={`transition-all duration-300 ${
 isCollapsed ? 'lg:ml-20' : 'lg:ml-64'
 }`}
 >
 <Header />
 <main className="p-4 lg:p-8">{children}</main>
 </div>
 </div>
 </SidebarContext.Provider>
 );
}

// Navigation items
const navItems = [
 { href: '/dashboard', label: 'Dashboard', icon: HomeIcon },
 { href: '/dashboard/analytics', label: 'Analytics', icon: ChartIcon },
 { href: '/dashboard/users', label: 'Users', icon: UsersIcon },
 { href: '/dashboard/settings', label: 'Settings', icon: SettingsIcon },
];

function Sidebar() {
 const pathname = usePathname();
 const { isCollapsed, isMobileOpen, toggleMobile } = useSidebar();

 return (
 <aside
 className={`
 fixed top-0 left-0 z-50 h-full bg-white border-r border-gray-200
 transition-all duration-300
 
 /* Mobile: slide in from left */
 ${isMobileOpen ? 'translate-x-0' : '-translate-x-full'}
 w-64
 
 /* Desktop: always visible, collapse to icons */
 lg:translate-x-0
 ${isCollapsed ? 'lg:w-20' : 'lg:w-64'}
 `}
 >
 {/* Logo */}
 <div className="h-16 flex items-center justify-between px-4 border-b border-gray-200">
 <Link href="/dashboard" className="flex items-center gap-2">
 <div className="w-8 h-8 bg-blue-600 rounded-lg" />
 {!isCollapsed && (
 <span className="font-semibold text-gray-900 hidden lg:block">AppName</span>
 )}
 </Link>
 
 {/* Mobile close button */}
 <button
 onClick={toggleMobile}
 className="lg:hidden p-2 text-gray-500 hover:text-gray-700"
 aria-label="Close menu"
 >
 <XIcon className="w-5 h-5" />
 </button>
 </div>

 {/* Navigation */}
 <nav className="p-4 space-y-1">
 {navItems.map((item) => {
 const isActive = pathname === item.href;
 return (
 <Link
 key={item.href}
 href={item.href}
 onClick={() => toggleMobile()}
 className={`
 flex items-center gap-3 px-3 py-2 rounded-lg transition-colors
 ${isActive
 ? 'bg-blue-50 text-blue-700'
 : 'text-gray-700 hover:bg-gray-100'
 }
 `}
 >
 <item.icon className="w-5 h-5 flex-shrink-0" />
 {(!isCollapsed || isMobileOpen) && (
 <span className="truncate">{item.label}</span>
 )}
 </Link>
 );
 })}
 </nav>
 </aside>
 );
}

function Header() {
 const { toggleMobile, toggleCollapse, isCollapsed } = useSidebar();

 return (
 <header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 lg:px-8">
 {/* Mobile menu button */}
 <button
 onClick={toggleMobile}
 className="lg:hidden p-2 text-gray-500 hover:text-gray-700"
 aria-label="Open menu"
 >
 <MenuIcon className="w-6 h-6" />
 </button>

 {/* Desktop collapse button */}
 <button
 onClick={toggleCollapse}
 className="hidden lg:block p-2 text-gray-500 hover:text-gray-700"
 aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
 >
 {isCollapsed ? (
 <ChevronRightIcon className="w-5 h-5" />
 ) : (
 <ChevronLeftIcon className="w-5 h-5" />
 )}
 </button>

 {/* Right side - user menu, etc */}
 <div className="flex items-center gap-4">
 <button className="p-2 text-gray-500 hover:text-gray-700">
 <BellIcon className="w-5 h-5" />
 </button>
 <div className="w-8 h-8 rounded-full bg-gray-200" />
 </div>
 </header>
 );
}

// Icon components (simplified)
function HomeIcon({ className }: { className?: string }) {
 return <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>;
}
function ChartIcon({ className }: { className?: string }) {
 return <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>;
}
function UsersIcon({ className }: { className?: string }) {
 return <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>;
}
function SettingsIcon({ className }: { className?: string }) {
 return <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>;
}
function MenuIcon({ className }: { className?: string }) {
 return <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /></svg>;
}
function XIcon({ className }: { className?: string }) {
 return <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>;
}
function ChevronLeftIcon({ className }: { className?: string }) {
 return <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg>;
}
function ChevronRightIcon({ className }: { className?: string }) {
 return <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>;
}
function BellIcon({ className }: { className?: string }) {
 return <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0.538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /></svg>;
}
```

When building application shells, dashboard layouts, or any responsive multi-section layouts

  • Use CSS transitions for smooth sidebar collapse, transform is more performant than width
  • Close mobile menu on navigation to prevent confusion when page changes

Common Mistakes to Avoid

Not specifying the framework version, React 18 patterns differ from React 17

Forgetting accessibility requirements, keyboard navigation, screen readers, focus management

Not handling all states, loading, error, empty, and success states are all needed

Frequently Asked Questions

Frontend development creates the user-facing layer of applications, what users see and interact with. These prompts help you build components, handle state, implement responsive designs, and solve common UI challenges. The key is specifying your framework, design requirements, and interaction patterns for code that fits your project.

Related Templates

Have your own prompt to optimize?