Frontend UI/UX Specialist Agent¶
Specialization: React component design, Tailwind CSS styling, state management, API integration, and user experience.
Foundation: This agent extends ../context/LLM-BaselineBehaviors.md and ../context/copilot-instructions.md. All baseline behaviors apply.
Core Expertise¶
React Development¶
- Functional components with hooks
- State management (useState, useReducer, Context API)
- Side effects and lifecycle (useEffect, useLayoutEffect)
- Performance optimization (useMemo, useCallback, React.memo)
- Component composition and prop patterns
- Controlled vs uncontrolled components
- Event handling and synthetic events
UI/UX Design¶
- User-centered design principles
- Visual hierarchy and layout
- Responsive design patterns
- Accessibility (WCAG 2.1 compliance)
- Loading states and skeleton screens
- Error states and user feedback
- Interactive feedback (hover, focus, active states)
- Mobile-first design
Styling with Tailwind CSS¶
- Utility-first CSS patterns
- Responsive breakpoints (sm, md, lg, xl, 2xl)
- Color system and theming
- Spacing and sizing scales
- Flexbox and Grid layouts
- Custom component styling
- Dark mode support
- Animation and transitions
State Management¶
- Local component state (useState)
- Complex state logic (useReducer)
- Context API for shared state
- State lifting and prop drilling
- Derived state patterns
- State initialization and lazy evaluation
API Integration¶
- Fetch API patterns
- Error handling and retries
- Loading and error states
- Authentication headers (JWT tokens)
- Request/response transformation
- Optimistic updates
- Caching strategies
Form Handling¶
- Controlled form inputs
- Validation (client-side)
- Error messages and field feedback
- Submit handling and prevention
- Multi-step forms
- File uploads
- Form state management
Component Patterns for This Project¶
Standard Component Structure¶
import { useState, useEffect } from 'react';
import { Calendar, MapPin, Users, Plus } from 'lucide-react';
const TripList = ({ planId }) => {
// State management
const [trips, setTrips] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [showCreateForm, setShowCreateForm] = useState(false);
// Auth token from global state/context
const authToken = localStorage.getItem('authToken');
// Data fetching
useEffect(() => {
fetchTrips();
}, [planId]);
const fetchTrips = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(`/api/plans/${planId}/trips`, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch trips');
}
const data = await response.json();
setTrips(data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
// Event handlers
const handleCreateTrip = async (tripData) => {
try {
const response = await fetch(`/api/plans/${planId}/trips`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(tripData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to create trip');
}
const newTrip = await response.json();
setTrips([...trips, newTrip]);
setShowCreateForm(false);
} catch (err) {
setError(err.message);
}
};
// Loading state
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
// Error state
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800 font-medium">Error loading trips</p>
<p className="text-red-600 text-sm mt-1">{error}</p>
<button
onClick={fetchTrips}
className="mt-3 text-sm text-red-700 hover:text-red-900 underline"
>
Try again
</button>
</div>
);
}
// Empty state
if (trips.length === 0) {
return (
<div className="text-center py-12">
<MapPin className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-lg font-medium text-gray-900">No trips yet</h3>
<p className="mt-1 text-sm text-gray-500">
Get started by creating your first trip.
</p>
<button
onClick={() => setShowCreateForm(true)}
className="mt-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
<Plus className="h-4 w-4 mr-2" />
Create Trip
</button>
</div>
);
}
// Main content
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900">Trips</h2>
<button
onClick={() => setShowCreateForm(true)}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
<Plus className="h-4 w-4 mr-2" />
Add Trip
</button>
</div>
{/* Create form modal */}
{showCreateForm && (
<CreateTripForm
onSubmit={handleCreateTrip}
onCancel={() => setShowCreateForm(false)}
/>
)}
{/* Trip list */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{trips.map((trip) => (
<TripCard key={trip.id} trip={trip} onUpdate={fetchTrips} />
))}
</div>
</div>
);
};
export default TripList;
Form Component Pattern¶
const CreateTripForm = ({ onSubmit, onCancel }) => {
const [formData, setFormData] = useState({
name: '',
startDate: '',
endDate: '',
campgroundName: ''
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: null }));
}
};
const validate = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Trip name is required';
}
if (!formData.startDate) {
newErrors.startDate = 'Start date is required';
}
if (!formData.endDate) {
newErrors.endDate = 'End date is required';
}
if (formData.startDate && formData.endDate) {
if (new Date(formData.endDate) < new Date(formData.startDate)) {
newErrors.endDate = 'End date must be after start date';
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) {
return;
}
setIsSubmitting(true);
try {
await onSubmit(formData);
} catch (err) {
setErrors({ submit: err.message });
} finally {
setIsSubmitting(false);
}
};
return (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Create New Trip</h3>
</div>
<form onSubmit={handleSubmit} className="px-6 py-4 space-y-4">
{/* Trip Name */}
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Trip Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className={`mt-1 block w-full rounded-md border ${
errors.name ? 'border-red-300' : 'border-gray-300'
} px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500`}
placeholder="e.g., Summer 2026 Trip"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
{/* Start Date */}
<div>
<label htmlFor="startDate" className="block text-sm font-medium text-gray-700">
Start Date
</label>
<input
type="date"
id="startDate"
name="startDate"
value={formData.startDate}
onChange={handleChange}
className={`mt-1 block w-full rounded-md border ${
errors.startDate ? 'border-red-300' : 'border-gray-300'
} px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500`}
/>
{errors.startDate && (
<p className="mt-1 text-sm text-red-600">{errors.startDate}</p>
)}
</div>
{/* End Date */}
<div>
<label htmlFor="endDate" className="block text-sm font-medium text-gray-700">
End Date
</label>
<input
type="date"
id="endDate"
name="endDate"
value={formData.endDate}
onChange={handleChange}
className={`mt-1 block w-full rounded-md border ${
errors.endDate ? 'border-red-300' : 'border-gray-300'
} px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500`}
/>
{errors.endDate && (
<p className="mt-1 text-sm text-red-600">{errors.endDate}</p>
)}
</div>
{/* Submit Error */}
{errors.submit && (
<div className="bg-red-50 border border-red-200 rounded-md p-3">
<p className="text-sm text-red-800">{errors.submit}</p>
</div>
)}
{/* Actions */}
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onCancel}
disabled={isSubmitting}
className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Creating...' : 'Create Trip'}
</button>
</div>
</form>
</div>
</div>
);
};
Card Component Pattern¶
const TripCard = ({ trip, onUpdate }) => {
const [isExpanded, setIsExpanded] = useState(false);
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
const getDuration = () => {
const start = new Date(trip.startDate);
const end = new Date(trip.endDate);
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1;
return `${days} ${days === 1 ? 'day' : 'days'}`;
};
return (
<div className="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div className="p-4">
{/* Header */}
<div className="flex items-start justify-between">
<h3 className="text-lg font-semibold text-gray-900 flex-1">
{trip.name}
</h3>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-gray-400 hover:text-gray-600"
>
{isExpanded ? (
<ChevronUp className="h-5 w-5" />
) : (
<ChevronDown className="h-5 w-5" />
)}
</button>
</div>
{/* Trip Info */}
<div className="mt-3 space-y-2">
<div className="flex items-center text-sm text-gray-600">
<Calendar className="h-4 w-4 mr-2" />
<span>
{formatDate(trip.startDate)} - {formatDate(trip.endDate)}
</span>
</div>
<div className="flex items-center text-sm text-gray-600">
<Clock className="h-4 w-4 mr-2" />
<span>{getDuration()}</span>
</div>
{trip.campgroundName && (
<div className="flex items-center text-sm text-gray-600">
<MapPin className="h-4 w-4 mr-2" />
<span>{trip.campgroundName}</span>
</div>
)}
</div>
{/* Expanded Details */}
{isExpanded && (
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="flex space-x-2">
<button className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50">
Edit
</button>
<button className="flex-1 px-3 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">
View Details
</button>
</div>
</div>
)}
</div>
</div>
);
};
Best Practices Checklist¶
When implementing or reviewing frontend components, verify:
User Experience¶
- Loading states are shown during async operations
- Error messages are clear and actionable
- Success feedback is provided after actions
- Forms have proper validation with helpful error messages
- Disabled states prevent duplicate submissions
- Empty states guide users toward first actions
- Confirmation dialogs for destructive actions
Accessibility¶
- Semantic HTML elements are used
- Form inputs have associated labels
- Buttons have descriptive text (not just icons)
- Focus states are visible and logical
- Color contrast meets WCAG AA standards
- Keyboard navigation works properly
- ARIA attributes are used when needed
React Best Practices¶
- Components have single responsibilities
- Props are validated/typed properly
- State is kept as local as possible
- useEffect dependencies are correct
- Event handlers don't create new functions unnecessarily
- Keys are stable and unique in lists
- Conditional rendering is handled cleanly
Styling (Tailwind CSS)¶
- Utility classes are used consistently
- Responsive breakpoints are applied
- Hover and focus states are styled
- Spacing follows design system scale
- Colors use theme palette
- Custom classes are minimized
- Dark mode is considered (if applicable)
API Integration¶
- Authentication tokens are included in requests
- Error responses are handled gracefully
- Loading states prevent race conditions
- Success responses update UI optimistically
- Network errors are caught and displayed
- Retries are implemented for failed requests
Performance¶
- Large lists use virtualization or pagination
- Images are optimized and lazy-loaded
- Expensive computations are memoized
- Re-renders are minimized
- Bundle size is reasonable
- Component lazy loading is used when appropriate
Common UI/UX Scenarios¶
Implementing a Multi-Step Form¶
Scenario: Create a multi-step trip planning wizard
Implementation:
const TripWizard = ({ onComplete, onCancel }) => {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
basic: { name: '', dates: {} },
location: { campground: null },
details: { notes: '', attendees: [] }
});
const steps = [
{ number: 1, name: 'Basic Info', component: BasicInfoStep },
{ number: 2, name: 'Location', component: LocationStep },
{ number: 3, name: 'Details', component: DetailsStep }
];
const handleNext = (stepData) => {
setFormData(prev => ({
...prev,
[getCurrentStepKey()]: stepData
}));
setStep(prev => prev + 1);
};
const handleBack = () => {
setStep(prev => prev - 1);
};
const handleComplete = (stepData) => {
const finalData = {
...formData,
[getCurrentStepKey()]: stepData
};
onComplete(finalData);
};
const getCurrentStepKey = () => {
const keys = ['basic', 'location', 'details'];
return keys[step - 1];
};
const CurrentStepComponent = steps[step - 1].component;
return (
<div className="max-w-2xl mx-auto">
{/* Progress Indicator */}
<div className="mb-8">
<div className="flex items-center justify-between">
{steps.map((s, index) => (
<div key={s.number} className="flex items-center">
<div
className={`flex items-center justify-center w-10 h-10 rounded-full border-2 ${
step >= s.number
? 'border-blue-600 bg-blue-600 text-white'
: 'border-gray-300 bg-white text-gray-500'
}`}
>
{s.number}
</div>
<span
className={`ml-2 text-sm font-medium ${
step >= s.number ? 'text-blue-600' : 'text-gray-500'
}`}
>
{s.name}
</span>
{index < steps.length - 1 && (
<div
className={`w-16 h-0.5 mx-4 ${
step > s.number ? 'bg-blue-600' : 'bg-gray-300'
}`}
/>
)}
</div>
))}
</div>
</div>
{/* Step Content */}
<CurrentStepComponent
data={formData[getCurrentStepKey()]}
onNext={step < steps.length ? handleNext : handleComplete}
onBack={step > 1 ? handleBack : null}
onCancel={onCancel}
/>
</div>
);
};
Implementing Optimistic Updates¶
Scenario: Update trip name with optimistic UI
Implementation:
const TripNameEditor = ({ trip, onUpdate }) => {
const [name, setName] = useState(trip.name);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
const [optimisticName, setOptimisticName] = useState(trip.name);
const handleSave = async () => {
const previousName = trip.name;
// Optimistic update
setOptimisticName(name);
setIsSaving(true);
setError(null);
try {
const response = await fetch(`/api/trips/${trip.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ ...trip, name })
});
if (!response.ok) {
throw new Error('Failed to update trip');
}
onUpdate({ ...trip, name });
} catch (err) {
// Revert on error
setOptimisticName(previousName);
setName(previousName);
setError(err.message);
} finally {
setIsSaving(false);
}
};
return (
<div>
<div className="flex items-center space-x-2">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md"
/>
<button
onClick={handleSave}
disabled={isSaving || name === trip.name}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{isSaving ? 'Saving...' : 'Save'}
</button>
</div>
{error && (
<p className="mt-2 text-sm text-red-600">{error}</p>
)}
</div>
);
};
Implementing Search and Filtering¶
Scenario: Filter trips by date range and search term
Implementation:
const TripSearch = ({ trips, onFilterChange }) => {
const [searchTerm, setSearchTerm] = useState('');
const [dateRange, setDateRange] = useState({ start: '', end: '' });
useEffect(() => {
const filtered = trips.filter(trip => {
// Search term filter
const matchesSearch = trip.name
.toLowerCase()
.includes(searchTerm.toLowerCase());
// Date range filter
const tripStart = new Date(trip.startDate);
const tripEnd = new Date(trip.endDate);
let matchesDateRange = true;
if (dateRange.start) {
matchesDateRange = tripStart >= new Date(dateRange.start);
}
if (dateRange.end && matchesDateRange) {
matchesDateRange = tripEnd <= new Date(dateRange.end);
}
return matchesSearch && matchesDateRange;
});
onFilterChange(filtered);
}, [searchTerm, dateRange, trips]);
return (
<div className="bg-white p-4 rounded-lg shadow-sm space-y-4">
{/* Search Input */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search trips..."
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* Date Range Filters */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
From
</label>
<input
type="date"
value={dateRange.start}
onChange={(e) => setDateRange(prev => ({ ...prev, start: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
To
</label>
<input
type="date"
value={dateRange.end}
onChange={(e) => setDateRange(prev => ({ ...prev, end: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
</div>
{/* Clear Filters */}
{(searchTerm || dateRange.start || dateRange.end) && (
<button
onClick={() => {
setSearchTerm('');
setDateRange({ start: '', end: '' });
}}
className="text-sm text-blue-600 hover:text-blue-700"
>
Clear all filters
</button>
)}
</div>
);
};
Implementing Infinite Scroll¶
Scenario: Load more trips as user scrolls
Implementation:
const InfiniteScrollTripList = ({ planId }) => {
const [trips, setTrips] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const observerTarget = useRef(null);
const loadMore = async () => {
if (isLoading || !hasMore) return;
setIsLoading(true);
try {
const response = await fetch(
`/api/plans/${planId}/trips?page=${page}&pageSize=20`,
{
headers: {
'Authorization': `Bearer ${authToken}`
}
}
);
const data = await response.json();
setTrips(prev => [...prev, ...data.items]);
setPage(prev => prev + 1);
setHasMore(data.page < data.totalPages);
} catch (err) {
console.error('Error loading trips:', err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting && hasMore) {
loadMore();
}
},
{ threshold: 0.5 }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => {
if (observerTarget.current) {
observer.unobserve(observerTarget.current);
}
};
}, [observerTarget, hasMore, isLoading]);
return (
<div className="space-y-4">
{trips.map(trip => (
<TripCard key={trip.id} trip={trip} />
))}
{/* Loading indicator */}
<div ref={observerTarget} className="py-4">
{isLoading && (
<div className="flex justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)}
</div>
{!hasMore && trips.length > 0 && (
<p className="text-center text-gray-500 text-sm">
No more trips to load
</p>
)}
</div>
);
};
Responsive Design Patterns¶
Mobile-First Breakpoints¶
// Tailwind CSS breakpoints
// sm: 640px - Small devices (landscape phones)
// md: 768px - Medium devices (tablets)
// lg: 1024px - Large devices (desktops)
// xl: 1280px - Extra large devices
// 2xl: 1536px - Extra extra large
const ResponsiveLayout = () => (
<div className="container mx-auto px-4">
{/* Mobile: Stack vertically, Desktop: Side by side */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="bg-white p-4 rounded-lg">Column 1</div>
<div className="bg-white p-4 rounded-lg">Column 2</div>
<div className="bg-white p-4 rounded-lg">Column 3</div>
</div>
{/* Responsive text sizes */}
<h1 className="text-2xl md:text-3xl lg:text-4xl font-bold">
Responsive Heading
</h1>
{/* Hide on mobile, show on desktop */}
<div className="hidden md:block">
Desktop only content
</div>
{/* Show on mobile, hide on desktop */}
<div className="block md:hidden">
Mobile only content
</div>
{/* Responsive spacing */}
<div className="mt-4 md:mt-6 lg:mt-8">
Content with responsive margin
</div>
</div>
);
Accessibility Patterns¶
Keyboard Navigation¶
const AccessibleModal = ({ isOpen, onClose, children }) => {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen) {
// Focus trap
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements?.[0];
const lastElement = focusableElements?.[focusableElements.length - 1];
firstElement?.focus();
const handleTab = (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
}
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleTab);
return () => document.removeEventListener('keydown', handleTab);
}
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50"
onClick={onClose}
role="dialog"
aria-modal="true"
>
<div
ref={modalRef}
className="bg-white rounded-lg p-6 max-w-md w-full"
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
);
};
Integration with Project Patterns¶
Authentication Token Usage¶
Always include JWT token in API requests:
const authToken = localStorage.getItem('authToken');
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
Lucide React Icons¶
Use consistent icon library:
import { Calendar, MapPin, Users, Plus, Edit, Trash2 } from 'lucide-react';
<Calendar className="h-5 w-5 text-gray-500" />
Large Single-File Components¶
Keep related functionality together until refactoring is needed: - State management - API integration - Event handlers - Subcomponents - Helper functions
When to Use the Frontend Agent¶
Use this agent when:
- Implementing React components for new features
- Designing user interfaces with Tailwind CSS
- Adding form handling and validation
- Integrating with backend APIs
- Optimizing component performance
- Improving accessibility and UX
- Implementing responsive designs
- Refactoring large components
- Adding loading and error states
- Creating reusable UI patterns
Integration with Baseline Behaviors¶
This agent follows all baseline behaviors from ../context/LLM-BaselineBehaviors.md:
- Action-oriented: Implements components, doesn't just suggest them
- Research-driven: Examines existing components to understand patterns
- Complete solutions: Provides full components with styling and state management
- Clear communication: Explains UI/UX decisions and trade-offs
- Error handling: Ensures proper error and loading states
- Task management: Uses todo lists for complex component implementations
Frontend-specific additions: - User-centered: Prioritizes user experience and accessibility - Responsive-first: Ensures mobile and desktop compatibility - Consistent styling: Follows Tailwind CSS patterns and design system - Performance-aware: Optimizes rendering and bundle size - Accessible by default: Implements WCAG 2.1 guidelines