| name | frontend-development |
| description | Comprehensive frontend development skill for building robust, responsive, and maintainable web applications and mobile apps. Use this for all UI/UX implementation, component building, styling, and client-side logic. |
Frontend Development Expert Skill
Core Principles
1. Code Quality & Architecture
- Write semantic, accessible HTML - Use proper tags (
<nav>, <main>, <article>, <section>, <button> not <div> with click handlers)
- Component-driven architecture - Break UI into reusable, single-responsibility components
- Separation of concerns - Keep logic, styling, and markup cleanly separated
- DRY (Don't Repeat Yourself) - Abstract repeated patterns into reusable utilities/components
- KISS (Keep It Simple, Stupid) - Avoid over-engineering; choose the simplest solution that works
2. Responsive Design (Mobile-First)
- Always start with mobile (320px) and scale up
- Breakpoint strategy:
.element { }
@media (min-width: 640px) { }
@media (min-width: 1024px) { }
@media (min-width: 1280px) { }
- Fluid typography: Use
clamp() for responsive font sizes
font-size: clamp(1rem, 2vw + 0.5rem, 1.5rem);
- Flexible layouts: Use
flexbox and grid instead of fixed widths
- Test on real devices - Emulators don't catch everything
DO's ✅
HTML
- ✅ Use semantic HTML5 elements
- ✅ Add ARIA labels for screen readers when needed
- ✅ Include
alt text for all images
- ✅ Use
<button> for actions, <a> for navigation
- ✅ Properly nest heading levels (h1 → h2 → h3)
- ✅ Add
lang attribute to <html> tag
- ✅ Use
<form> with proper label associations
CSS
- ✅ Use CSS variables for theming
:root {
--primary-color: #007bff;
--spacing-unit: 8px;
}
- ✅ Employ BEM or consistent naming convention
.card { }
.card__title { }
.card--featured { }
- ✅ Use modern layout tools (Grid, Flexbox)
- ✅ Implement responsive images
<img srcset="image-320w.jpg 320w, image-640w.jpg 640w"
sizes="(max-width: 640px) 100vw, 640px"
src="image-640w.jpg" alt="Description">
- ✅ Use relative units (
rem, em, %, vw, vh) over px
- ✅ Add
:focus styles for keyboard navigation
- ✅ Use CSS Grid for 2D layouts, Flexbox for 1D
JavaScript/TypeScript
React/Component Frameworks
Performance
- ✅ Lazy load images and components
- ✅ Minimize bundle size (tree shaking, code splitting)
- ✅ Use CDN for static assets
- ✅ Implement virtual scrolling for long lists
- ✅ Optimize images (WebP, proper sizing, compression)
- ✅ Cache API responses appropriately
- ✅ Use lighthouse/web vitals for monitoring
Accessibility (a11y)
- ✅ Ensure keyboard navigation works (Tab, Enter, Escape, Arrow keys)
- ✅ Maintain 4.5:1 contrast ratio for text
- ✅ Add skip-to-content links
- ✅ Test with screen readers (NVDA, JAWS, VoiceOver)
- ✅ Use proper heading hierarchy
- ✅ Provide text alternatives for non-text content
DON'Ts ❌
HTML
- ❌ Don't use tables for layout (only for tabular data)
- ❌ Don't use
<div> or <span> when semantic elements exist
- ❌ Don't omit closing tags (even optional ones)
- ❌ Don't use inline styles (use CSS classes)
- ❌ Don't use deprecated tags (
<center>, <font>, <marquee>)
CSS
JavaScript
- ❌ Don't mutate state directly (use immutable patterns)
state.items.push(newItem);
setState({ items: [...state.items, newItem] });
- ❌ Don't use
eval() or innerHTML with user input (XSS vulnerability)
- ❌ Don't ignore error handling
- ❌ Don't make synchronous API calls (blocks UI)
- ❌ Don't use global variables
- ❌ Don't compare with
== (use strict ===)
- ❌ Don't forget to clean up event listeners/subscriptions
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
- ❌ Don't use
any type in TypeScript (defeats the purpose)
- ❌ Don't use array index as key in React lists (causes bugs)
Performance
- ❌ Don't load entire libraries for one function (use tree-shaking or alternatives)
- ❌ Don't make unnecessary re-renders
- ❌ Don't fetch data in loops
- ❌ Don't block the main thread with heavy computations (use Web Workers)
- ❌ Don't ignore bundle size (keep < 200KB initial load)
Security
- ❌ Don't store sensitive data in localStorage (use httpOnly cookies)
- ❌ Don't trust user input
- ❌ Don't expose API keys in frontend code
- ❌ Don't use outdated dependencies (security vulnerabilities)
Responsive Patterns
Container Queries (Modern Approach)
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 1fr 2fr;
}
}
Fluid Grids
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
Responsive Typography
html {
font-size: 16px;
}
@media (min-width: 640px) {
html { font-size: 18px; }
}
h1 { font-size: clamp(1.5rem, 5vw, 3rem); }
Touch-Friendly Targets
.button {
min-height: 44px;
min-width: 44px;
padding: 12px 24px;
}
Refactoring Guidelines
When to Refactor
- Code duplication - Extract into reusable component/utility
- Component > 250 lines - Split into smaller components
- Props drilling > 3 levels - Use context or state management
- Complex conditional rendering - Extract into separate components
- Performance issues - Profile and optimize
Refactoring Process
- Write/ensure tests exist - Don't refactor without safety net
- One change at a time - Small, incremental changes
- Commit frequently - Easy to rollback if needed
- Test after each change - Catch regressions early
- Update documentation - Keep comments/docs in sync
Extract Component Pattern
function UserProfile() {
return (
<div>
{/* 200 lines of JSX */}
</div>
);
}
function UserProfile() {
return (
<div>
<UserHeader />
<UserStats />
<UserPosts />
</div>
);
}
Extract Hook Pattern (with TanStack Query)
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetch(`/api/search?q=${query}`).then(...);
}, [query]);
}
import { useQuery } from '@tanstack/react-query';
import { useDebouncedValue } from '@/hooks/use-debounce';
function useSearch(query: string) {
const debouncedQuery = useDebouncedValue(query, 300);
return useQuery({
queryKey: ['search', debouncedQuery],
queryFn: () => searchApi(debouncedQuery),
enabled: debouncedQuery.length > 2,
staleTime: 5 * 60 * 1000,
});
}
function SearchPage() {
const [query, setQuery] = useState('');
const { data: results, isLoading, error } = useSearch(query);
}
Testing Best Practices
What to Test
- ✅ User interactions (clicks, form submissions)
- ✅ Conditional rendering
- ✅ Edge cases and error states
- ✅ Accessibility (keyboard navigation, screen reader output)
- ✅ Responsive behavior (viewport changes)
Testing Pattern
test('submits form with user data', async () => {
const handleSubmit = jest.fn();
render(<SignupForm onSubmit={handleSubmit} />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'password123');
await userEvent.click(screen.getByRole('button', { name: /sign up/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
State Management Rules
Local State (useState)
- Use for: UI state, form inputs, toggles
- Keep state close to where it's used
Context API
- Use for: Theme, auth, localization
- Don't overuse - causes unnecessary re-renders
External State (Redux, Zustand, Jotai)
- Use for: Complex app state, data shared across many components
- Keep actions pure and predictable
Server State (React Query, SWR)
- Use for: API data, caching, background updates
- Let library handle loading/error states
Mobile App Considerations (React Native / PWA)
React Native Specific
- ✅ Use
StyleSheet.create() for performance
- ✅ Use
FlatList for long lists (not ScrollView)
- ✅ Optimize images with
resizeMode
- ✅ Handle keyboard properly with
KeyboardAvoidingView
- ❌ Don't use web-specific CSS
- ❌ Don't forget platform-specific code when needed
PWA Best Practices
- ✅ Register service worker for offline support
- ✅ Add web app manifest
- ✅ Implement proper caching strategies
- ✅ Handle offline state gracefully
- ✅ Add install prompt
File Structure (Scalable)
src/
├── components/
│ ├── common/ # Reusable UI components
│ ├── features/ # Feature-specific components
│ └── layout/ # Layout components
├── hooks/ # Custom hooks
├── utils/ # Helper functions
├── services/ # API calls
├── store/ # State management
├── styles/ # Global styles, themes
├── types/ # TypeScript types
├── constants/ # Configuration
└── tests/ # Test utilities
Code Review Checklist
Before submitting code, verify:
Common Pitfalls to Avoid
- Premature optimization - Profile first, then optimize
- Over-abstraction - Don't abstract until pattern emerges 3+ times
- Ignoring browser compatibility - Check caniuse.com for newer features
- Not testing on real devices - Emulators miss important issues
- Skipping accessibility - Bake it in from the start
- Tight coupling - Keep components loosely coupled
- Magic numbers - Use named constants
- Inconsistent naming - Follow a convention strictly
Performance Optimization Checklist
Quick Reference: Modern CSS Features
@container (min-width: 400px) { }
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
display: flex;
gap: 1rem;
aspect-ratio: 16 / 9;
font-size: clamp(1rem, 2.5vw, 2rem);
color: var(--primary-color, #007bff);
margin-inline: auto;
padding-block: 2rem;
:is(header, footer) a { }
:where(nav) a { }
:has(img) { }
Data Fetching
Current State (Migration in Progress)
This project has TanStack Query installed and hooks ready in use-event-crud.ts, but components currently use useEffect + useState patterns. New code should use the TanStack Query hooks.
Available TanStack Query Hooks (use-event-crud.ts)
import {
useEvents,
useEvent,
useCreateEvent,
useUpdateEvent,
useDeleteEvent,
useEventSearch,
useNearbyEvents,
} from '@/hooks/use-event-crud';
function EventList() {
const { data, isLoading, error } = useEvents({
filters: { status: 'published' },
limit: 100,
});
if (isLoading) return <EventListSkeleton />;
if (error) return <ErrorState error={error} />;
return <EventGrid events={data?.events} />;
}
Legacy Pattern (Current - To Be Migrated)
const [events, setEvents] = useState<EventType[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchEventsData = async () => {
setIsLoading(true);
try {
const response = await fetchEvents(queryParams);
setEvents(response.events);
} finally {
setIsLoading(false);
}
};
fetchEventsData();
}, [dependencies]);
Target Pattern (For New Code)
import { useEvents, useCreateEvent } from '@/hooks/use-event-crud';
function EventsContainer({ filters }: Props) {
const { data, isLoading, error, refetch } = useEvents({
filters,
limit: 100,
});
const createMutation = useCreateEvent({
onSuccess: () => {
toast.success('Event created!');
},
});
if (isLoading) return <EventListSkeleton />;
if (error) return <ErrorState error={error} />;
return <EventGrid events={data?.events} />;
}
Why TanStack Query is Better
| Feature | useEffect + useState | TanStack Query |
|---|
| Caching | Manual | Automatic |
| Deduplication | None | Built-in |
| Race conditions | Must handle | Handled |
| Background refetch | Manual | Automatic |
| Optimistic updates | Complex | Simple API |
| Loading/Error states | Manual | Built-in |
| DevTools | None | Excellent |
Migration Priority
- High:
EventsContainer.tsx - Main events display
- Medium:
LocationListingPage.tsx - Location-based events
- Low:
UnifiedSearch.tsx - Search component
Deprecated Hooks (Do Not Use)
import { useEvents, useEvent, useCreateEvent } from '@/hooks/use-supabase';
Form Handling (React Hook Form + Zod)
RULE: Never use useState for complex forms. Use React Hook Form with Zod validation.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const eventSchema = z.object({
title: z.string().min(3, 'Title must be at least 3 characters'),
description: z.string().max(500),
date: z.string().datetime(),
location: z.object({
lat: z.number(),
lng: z.number(),
}),
foodType: z.enum(['veg', 'non-veg', 'vegan', 'other']),
});
type EventFormData = z.infer<typeof eventSchema>;
function EventForm({ onSubmit }: { onSubmit: (data: EventFormData) => void }) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<EventFormData>({
resolver: zodResolver(eventSchema),
defaultValues: {
title: '',
description: '',
},
});
const submitHandler = async (data: EventFormData) => {
try {
await onSubmit(data);
reset();
showSuccessToast('Event created!');
} catch (error) {
showErrorToast('Failed to create event');
}
};
return (
<form onSubmit={handleSubmit(submitHandler)}>
<div>
<label htmlFor="title">Title</label>
<input id="title" {...register('title')} />
{errors.title && <span role="alert">{errors.title.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create Event'}
</button>
</form>
);
}
Next.js Specific Patterns
Always Use <Image />, Never <img>
import Image from 'next/image';
<img src="/hero.jpg" alt="Hero" />
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // For above-the-fold images
placeholder="blur"
blurDataURL={blurHash}
/>
Server vs Client Components
async function EventPage({ params }: { params: { id: string } }) {
const event = await fetchEvent(params.id);
return <EventDetails event={event} />;
}
"use client";
function LikeButton({ eventId }: { eventId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>Like</button>;
}
Keep Client Boundaries Small
"use client";
export default function EventPage() {
}
export default async function EventPage() {
const event = await fetchEvent();
return (
<div>
<EventHeader event={event} /> {/* Server */}
<EventDetails event={event} /> {/* Server */}
<LikeButton eventId={event.id} /> {/* Client - "use client" inside */}
</div>
);
}
URL State with nuqs (Not useState)
import { useQueryState, parseAsInteger } from 'nuqs';
const [page, setPage] = useState(1);
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
Loading States (Skeletons, Not Spinners)
RULE: Never use a single spinner for the whole page. Use skeleton loaders that match the layout.
function EventList() {
if (isLoading) return <Spinner />;
return <div>...</div>;
}
function EventListSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="animate-pulse">
<div className="bg-gray-200 h-48 rounded-lg" />
<div className="mt-4 h-4 bg-gray-200 rounded w-3/4" />
<div className="mt-2 h-4 bg-gray-200 rounded w-1/2" />
</div>
))}
</div>
);
}
function EventList() {
const { data, isLoading } = useQuery({ ... });
if (isLoading) return <EventListSkeleton />;
return <EventGrid events={data} />;
}
Next.js Loading States
export default function Loading() {
return <EventListSkeleton />;
}
Error Boundaries
RULE: Wrap risky components so errors don't crash the entire app.
"use client";
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('Error boundary caught:', error, info);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div role="alert" className="p-4 bg-red-50 text-red-800 rounded">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
<ErrorBoundary fallback={<EventErrorFallback />}>
<EventDetails event={event} />
</ErrorBoundary>
Next.js Error Handling
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div role="alert">
<h2>Failed to load events</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
Third-Party Library DOM Integration (Critical)
RULE: Never put React children inside a container that a third-party library (Google Maps, D3, Three.js, etc.) will manipulate.
The Problem
Third-party libraries take ownership of their container's DOM. If React also tries to manage children inside that container, you get removeChild errors and crashes.
function BadMap() {
const mapRef = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
new google.maps.Map(mapRef.current!, { ... });
}, []);
return (
<div ref={mapRef}>
{/* React children inside third-party container = CRASH */}
{loading && <Spinner />}
{error && <ErrorMessage />}
</div>
);
}
The Solution - Empty Container with Absolute Siblings
function GoodMap() {
const mapRef = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
new google.maps.Map(mapRef.current!, { ... });
setLoading(false);
}, []);
return (
<div className="relative w-full h-full">
{/* Empty self-closing container - library owns this completely */}
<div ref={mapRef} className="absolute inset-0" />
{/* React-managed states as SIBLINGS with absolute positioning */}
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/80 z-10">
<Spinner />
</div>
)}
{error && (
<div className="absolute inset-0 flex items-center justify-center bg-red-50 z-10">
<ErrorMessage error={error} />
</div>
)}
</div>
);
}
Rules for Third-Party Integration
- Empty container: Library containers MUST be self-closing
<div ref={ref} />
- No React children: Never put conditional renders inside the container
- Absolute siblings: UI overlays must be siblings with
position: absolute
- Parent relative: Wrapper needs
position: relative for positioning context
- Z-index layers: Use
z-10 or higher for overlays to appear above library content
- Single ownership: Each DOM subtree has ONE owner (React OR third-party, never both)
When This Applies
- Google Maps (
@googlemaps/js-api-loader)
- Leaflet maps
- D3.js visualizations
- Three.js / React Three Fiber (for custom DOM overlays)
- Any library that calls
appendChild, removeChild, or innerHTML
Toast Notifications (Never window.alert)
RULE: Never use window.alert(). Always use toast notifications.
import { toast } from 'sonner';
toast.success('Event created successfully!');
toast.error('Failed to save changes');
toast('Event deleted', {
action: {
label: 'Undo',
onClick: () => restoreEvent(eventId),
},
});
toast.promise(createEvent(data), {
loading: 'Creating event...',
success: 'Event created!',
error: 'Failed to create event',
});
Related Skills
For foundational principles and backend patterns, see:
Summary
Write code that is:
- 📱 Responsive (mobile-first, fluid, tested)
- ♿ Accessible (semantic, keyboard-friendly, screen-reader compatible)
- ⚡ Performant (optimized, lazy-loaded, cached)
- 🧪 Testable (small components, pure functions, good separation)
- 📖 Readable (consistent naming, proper comments, self-documenting)
- 🔒 Secure (validated input, no XSS, updated dependencies)
- 🔄 Maintainable (DRY, KISS, proper abstraction)
When in doubt:
- Start simple
- Make it work
- Make it right
- Make it fast (only if needed)