From react-developer
Implement a React component from a design specification or component-spec output.
npx claudepluginhub hpsgd/turtlestack --plugin react-developerThis skill is limited to using the following tools:
Implement a React component from the specification at $ARGUMENTS.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Designs, implements, and audits WCAG 2.2 AA accessible UIs for Web (ARIA/HTML5), iOS (SwiftUI traits), and Android (Compose semantics). Audits code for compliance gaps.
Implement a React component from the specification at $ARGUMENTS.
Before writing any code, examine existing components to match project conventions:
Scan for patterns:
# Find existing components to match conventions
ls -la src/components/ 2>/dev/null || ls -la app/components/ 2>/dev/null
Identify and match:
index.ts)?Read the spec completely: Props, variants, states, responsive behaviour, accessibility requirements, interactions, animations
Rules:
Before writing JSX, design the component API:
Props interface (TypeScript MANDATORY):
export interface ComponentNameProps {
/** Required props first, grouped by purpose */
title: string;
items: Item[];
/** Variant prop for visual modes */
variant?: 'default' | 'compact' | 'expanded';
/** State props */
isLoading?: boolean;
isDisabled?: boolean;
/** Event handlers — use standard naming */
onSelect?: (item: Item) => void;
onDismiss?: () => void;
/** Composition props */
className?: string;
children?: React.ReactNode;
}
Rules:
{ComponentName}Props and exportedvariant, not type or modeis/has/can prefix: isLoading, hasError, canEditon prefix: onSelect, onChange, onDismissclassName for composition (merged via clsx)Write tests BEFORE implementation. Test behaviour, not implementation details.
Test file: component-name.test.ts (co-located)
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ComponentName } from './component-name';
describe('ComponentName', () => {
it('renders the title', () => {
render(<ComponentName title="Hello" items={[]} />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
it('calls onSelect when an item is clicked', async () => {
const onSelect = vi.fn();
const items = [{ id: '1', label: 'Item 1' }];
render(<ComponentName title="Test" items={items} onSelect={onSelect} />);
await userEvent.click(screen.getByText('Item 1'));
expect(onSelect).toHaveBeenCalledWith(items[0]);
});
it('renders loading skeleton when isLoading is true', () => {
render(<ComponentName title="Test" items={[]} isLoading />);
expect(screen.getByRole('status')).toBeInTheDocument();
});
it('renders empty state when items is empty and not loading', () => {
render(<ComponentName title="Test" items={[]} />);
expect(screen.getByText(/no items/i)).toBeInTheDocument();
});
});
Rules:
userEvent (not fireEvent) for user interactionsComponent structure:
import { clsx } from 'clsx';
export interface ComponentNameProps {
// ... (from Step 2)
}
export function ComponentName({
title,
items,
variant = 'default',
isLoading = false,
isDisabled = false,
onSelect,
className,
}: ComponentNameProps) {
// 1. Hooks first (useState, useEffect, custom hooks)
// 2. Derived state (computed from props/state, no useMemo unless measured)
// 3. Event handlers
// 4. Early returns for special states
if (isLoading) {
return <ComponentNameSkeleton />;
}
if (items.length === 0) {
return <ComponentNameEmpty />;
}
// 5. Main render
return (
<div className={clsx('base-classes', variantClasses[variant], className)}>
{/* ... */}
</div>
);
}
Rules:
function ComponentName), not arrow functions, for top-level componentsuseMemo unless profiling shows a performance problemuseCallback only when passing to memoised children or in dependency arraysclsx for conditional class composition — never string interpolation| State | Must handle | Implementation |
|---|---|---|
| Default | Always | Normal rendering with data |
| Loading | If data is async | Skeleton or spinner with role="status" and aria-label |
| Error | If data can fail | Error message with retry action |
| Empty | If collection can be empty | Meaningful empty state, not just blank space |
| Disabled | If interactive | Reduced opacity, no hover effects, aria-disabled="true" |
| Hover/Focus | If interactive | Visual feedback via CSS :hover/:focus-visible |
| Active/Selected | If selectable | aria-selected="true", visual indicator |
| Overflow | If content varies | Text truncation, scrollable containers, responsive layout |
Rules:
| Requirement | Implementation | Test |
|---|---|---|
| Semantic HTML | Use <button>, <nav>, <main>, <article>, not <div onClick> | Query by role in tests |
| ARIA roles | role="status" for loading, role="alert" for errors, role="list" for lists | Verify with getByRole |
| Keyboard navigation | Tab order, Enter/Space for activation, Escape for dismissal, Arrow keys for lists | Test with userEvent.keyboard |
| Focus management | Focus trap in modals, focus restoration on close, visible focus ring | Test focus element after interaction |
| Screen reader text | aria-label for icon buttons, aria-live for dynamic content | Verify ARIA attributes |
| Colour contrast | Minimum 4.5:1 for text, 3:1 for large text | Manual check against design |
| Motion | prefers-reduced-motion media query for animations | CSS media query |
Rules:
<button>, not a <div> or <span><a>, not a <button>alt text (empty string alt="" for decorative images)aria-label if visually hidden)focus-visible, not focus)| Breakpoint | Approach |
|---|---|
| Mobile-first | Default styles are mobile. Add complexity with sm:, md:, lg: |
| Touch targets | Minimum 44x44px for interactive elements on mobile |
| Layout shifts | No content jumps on load — reserve space with skeletons |
| Text overflow | Truncate with ellipsis or expand/collapse, never horizontal scroll |
| Images | next/image with sizes attribute, responsive source sets |
Tailwind breakpoint usage:
<div className="flex flex-col gap-2 md:flex-row md:gap-4 lg:gap-6">
<div className="w-full md:w-1/3 lg:w-1/4">Sidebar</div>
<div className="w-full md:w-2/3 lg:w-3/4">Content</div>
</div>
Rules:
w-[347px]) unless the design system requires exact pixels<div> for everything. Use semantic HTML elementsuseEffect is for side effects, not data transformationtext-[13.5px] is a code smell. Use standard scale valuesDeliver:
component-name.tsx) with typed props, all states, accessibilitycomponent-name.test.ts) co-located, testing behaviour not implementationindex.ts)/ui-designer:component-spec — this skill consumes the component spec as input. Write the spec first, then build the component from it.