frontend-developer
SKILL.md
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);
Weekly Installs
2
Repository
arosenkranz/cla…e-configGitHub Stars
1
First Seen
Feb 27, 2026
Security Audits
Installed on
cline2
gemini-cli2
github-copilot2
codex2
kimi-cli2
cursor2