frontend-developer
Frontend Developer
Guidelines for modern React development with accessibility and performance.
Core Principles
- Component-first - Reusable, composable UI pieces
- Mobile-first - Design for small screens, enhance for larger
- Accessible by default - WCAG compliance from the start
- Type-safe props - TypeScript interfaces for all components
- Performance budgets - Aim for sub-3s load times
Component Patterns
Typed Props with Children
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export function Button({
variant = 'primary',
size = 'md',
isLoading = false,
disabled = false,
onClick,
children,
}: ButtonProps) {
return (
<button
className={cn(
'rounded-md font-medium transition-colors',
variants[variant],
sizes[size],
(disabled || isLoading) && 'opacity-50 cursor-not-allowed'
)}
disabled={disabled || isLoading}
onClick={onClick}
aria-busy={isLoading}
>
{isLoading ? <Spinner /> : children}
</button>
);
}
Compound Components
interface TabsContextType {
activeTab: string;
setActiveTab: (id: string) => void;
}
const TabsContext = createContext<TabsContextType | null>(null);
function Tabs({ children, defaultValue }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div role="tablist">{children}</div>
</TabsContext.Provider>
);
}
function TabTrigger({ value, children }: TabTriggerProps) {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error('TabTrigger must be inside Tabs');
return (
<button
role="tab"
aria-selected={ctx.activeTab === value}
onClick={() => ctx.setActiveTab(value)}
>
{children}
</button>
);
}
Tabs.Trigger = TabTrigger;
Tabs.Content = TabContent;
Hooks Patterns
Custom Data Fetching Hook
function useFetch<T>(url: string) {
const [state, setState] = useState<{
data: T | null;
isLoading: boolean;
error: Error | null;
}>({
data: null,
isLoading: true,
error: null,
});
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setState({ data, isLoading: false, error: null });
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setState({ data: null, isLoading: false, error: err });
}
}
}
fetchData();
return () => controller.abort();
}, [url]);
return state;
}
Debounced Value Hook
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
Local Storage Hook
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue] as const;
}
Accessibility Checklist
Semantic HTML
- Use
<button>for actions,<a>for navigation - Use heading hierarchy (
h1->h2->h3) - Use
<nav>,<main>,<aside>,<footer>landmarks
ARIA Attributes
// Announce dynamic content
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// Label interactive elements
<button aria-label="Close dialog" aria-describedby="dialog-desc">
<XIcon />
</button>
// Indicate states
<button aria-pressed={isActive} aria-expanded={isOpen}>
Menu
</button>
Keyboard Navigation
function handleKeyDown(e: KeyboardEvent) {
switch (e.key) {
case 'ArrowDown':
focusNext();
break;
case 'ArrowUp':
focusPrev();
break;
case 'Escape':
close();
break;
case 'Enter':
case ' ':
select();
break;
}
}
Focus Management
// Focus trap in modals
useEffect(() => {
if (isOpen) {
const previousFocus = document.activeElement as HTMLElement;
firstFocusableRef.current?.focus();
return () => previousFocus?.focus();
}
}, [isOpen]);
Tailwind Patterns
Responsive Design
<div className="
grid
grid-cols-1
sm:grid-cols-2
lg:grid-cols-3
gap-4
p-4
sm:p-6
lg:p-8
">
{items.map(item => <Card key={item.id} {...item} />)}
</div>
Component Variants with CVA
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
ghost: 'hover:bg-gray-100',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
type ButtonProps = VariantProps<typeof buttonVariants> & {
children: React.ReactNode;
};
Performance Optimization
Code Splitting
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Skeleton />}>
<HeavyComponent />
</Suspense>
);
}
Memoization
// Memoize expensive calculations
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// Memoize callbacks passed to children
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
// Memoize components that receive stable props
const MemoizedChild = memo(function Child({ data }: Props) {
return <div>{data.name}</div>;
});
Image Optimization
<img
src={imageSrc}
alt={imageAlt}
loading="lazy"
decoding="async"
width={400}
height={300}
/>
State Management
Context + Reducer Pattern
type Action =
| { type: 'ADD_ITEM'; payload: Item }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'CLEAR' };
function cartReducer(state: CartState, action: Action): CartState {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter(i => i.id !== action.payload) };
case 'CLEAR':
return { ...state, items: [] };
}
}
const CartContext = createContext<{
state: CartState;
dispatch: Dispatch<Action>;
} | null>(null);
More from arosenkranz/claude-code-config
homelab-helper
Expert guidance for homelab infrastructure, self-hosting, and Raspberry Pi optimization. Use when recommending self-hosted services, configuring Docker services, setting up reverse proxies, integrating Home Assistant, or troubleshooting homelab networking.
17continuous-learning-v2
Instinct-based learning system that observes sessions via hooks, creates atomic instincts with confidence scoring, and evolves them into skills/commands/agents.
6session-log
Document conversation accomplishments in Obsidian vault with frontmatter, code changes, learning notes, and next steps.
6evolve
Cluster related instincts into skills, commands, or agents
6reclaude
Refactor CLAUDE.md files to follow progressive disclosure principles. Use when a CLAUDE.md file is too long or needs organizational improvement.
5find-skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
5