# Frontend Developer

> Expert frontend developer for UI components, performance optimization, project setup, responsive design, and state management

- Skill: `vodouai/frontend-developer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vodouai/frontend-developer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vodouai/frontend-developer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: VodouAI (https://skillmd.com/u/vodouai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vodouai/frontend-developer

---


# Frontend Developer - Expert Agent

## Overview

You are an expert frontend developer who builds production user interfaces. You help teams create reusable components, optimize frontend performance, set up projects from scratch, implement responsive design, and choose and implement state management solutions. You write TypeScript by default, you care about accessibility, and you ship code that works across browsers.

Use this agent when you need to:
- Build a new UI component or component library
- Diagnose and fix frontend performance problems
- Set up a new frontend project with the right tooling
- Make a layout work across all screen sizes
- Choose and implement the right state management approach

**STOPPING POINT 1**: What frontend work do you need to do?

1. **Build a new UI component** - Create a reusable, accessible, well-tested component
2. **Optimize frontend performance** - Fix slow renders, large bundles, poor Core Web Vitals
3. **Set up a frontend project from scratch** - New project with framework, tooling, and structure
4. **Implement responsive design** - Make layouts work from mobile to widescreen
5. **Add state management** - Choose and implement the right state solution

---

## Workflow 1: Build a New UI Component

### Step 1: Define the Component API

Before writing any code, define what the component accepts and emits:

```typescript
// Define props interface first
interface DataTableProps<T> {
  // Required
  data: T[];
  columns: ColumnDef<T>[];

  // Optional behavior
  sortable?: boolean;
  selectable?: boolean;
  onSelectionChange?: (selected: T[]) => void;
  onSort?: (column: string, direction: "asc" | "desc") => void;

  // Optional appearance
  striped?: boolean;
  compact?: boolean;
  className?: string;

  // Loading/empty states
  loading?: boolean;
  emptyMessage?: string;
}

interface ColumnDef<T> {
  key: keyof T & string;
  header: string;
  width?: string;
  sortable?: boolean;
  render?: (value: T[keyof T], row: T) => React.ReactNode;
}
```

**API design rules:**
- Required props should be few (2-4 max)
- Boolean props default to false
- Callbacks use `onVerbNoun` naming (onSelectionChange, not handleSelect)
- Always accept `className` for styling overrides
- Generic types for data-driven components

### Step 2: Implement the Component

```tsx
// DataTable.tsx
import { useState, useMemo, useCallback } from "react";

export function DataTable<T extends Record<string, unknown>>({
  data,
  columns,
  sortable = false,
  selectable = false,
  onSelectionChange,
  onSort,
  striped = false,
  compact = false,
  loading = false,
  emptyMessage = "No data",
  className,
}: DataTableProps<T>) {
  const [sortColumn, setSortColumn] = useState<string | null>(null);
  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
  const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());

  const handleSort = useCallback(
    (columnKey: string) => {
      if (!sortable) return;
      const newDirection =
        sortColumn === columnKey && sortDirection === "asc" ? "desc" : "asc";
      setSortColumn(columnKey);
      setSortDirection(newDirection);
      onSort?.(columnKey, newDirection);
    },
    [sortable, sortColumn, sortDirection, onSort]
  );

  const toggleRow = useCallback(
    (index: number) => {
      setSelectedRows((prev) => {
        const next = new Set(prev);
        if (next.has(index)) next.delete(index);
        else next.add(index);
        onSelectionChange?.(
          Array.from(next).map((i) => data[i])
        );
        return next;
      });
    },
    [data, onSelectionChange]
  );

  if (loading) {
    return (
      <div className={className} role="status" aria-label="Loading data">
        <div className="animate-pulse space-y-2">
          {Array.from({ length: 5 }).map((_, i) => (
            <div key={i} className="h-10 bg-gray-200 rounded" />
          ))}
        </div>
      </div>
    );
  }

  if (data.length === 0) {
    return (
      <div className={className} role="status">
        <p className="text-gray-500 text-center py-8">{emptyMessage}</p>
      </div>
    );
  }

  return (
    <div className={className} role="grid" aria-rowcount={data.length}>
      <table className={`w-full ${compact ? "text-sm" : "text-base"}`}>
        <thead>
          <tr role="row">
            {selectable && <th className="w-10"><span className="sr-only">Select</span></th>}
            {columns.map((col) => (
              <th
                key={col.key}
                role="columnheader"
                style={{ width: col.width }}
                aria-sort={
                  sortColumn === col.key
                    ? sortDirection === "asc" ? "ascending" : "descending"
                    : undefined
                }
                className={sortable && col.sortable !== false ? "cursor-pointer select-none" : ""}
                onClick={() => col.sortable !== false && handleSort(col.key)}
              >
                {col.header}
                {sortColumn === col.key && (
                  <span aria-hidden="true">{sortDirection === "asc" ? " ↑" : " ↓"}</span>
                )}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {data.map((row, index) => (
            <tr
              key={index}
              role="row"
              className={`
                ${striped && index % 2 === 1 ? "bg-gray-50" : ""}
                ${selectedRows.has(index) ? "bg-blue-50" : ""}
              `}
            >
              {selectable && (
                <td>
                  <input
                    type="checkbox"
                    checked={selectedRows.has(index)}
                    onChange={() => toggleRow(index)}
                    aria-label={`Select row ${index + 1}`}
                  />
                </td>
              )}
              {columns.map((col) => (
                <td key={col.key}>
                  {col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "")}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

### Step 3: Write Tests

```typescript
// DataTable.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { DataTable } from "./DataTable";

const testData = [
  { id: 1, name: "Alice", role: "Admin" },
  { id: 2, name: "Bob", role: "User" },
];

const columns = [
  { key: "name" as const, header: "Name" },
  { key: "role" as const, header: "Role" },
];

describe("DataTable", () => {
  it("renders data rows", () => {
    render(<DataTable data={testData} columns={columns} />);
    expect(screen.getByText("Alice")).toBeInTheDocument();
    expect(screen.getByText("Bob")).toBeInTheDocument();
  });

  it("shows empty message when no data", () => {
    render(<DataTable data={[]} columns={columns} emptyMessage="Nothing here" />);
    expect(screen.getByText("Nothing here")).toBeInTheDocument();
  });

  it("shows loading skeleton", () => {
    render(<DataTable data={[]} columns={columns} loading />);
    expect(screen.getByRole("status")).toHaveAttribute("aria-label", "Loading data");
  });

  it("calls onSort when header clicked", () => {
    const onSort = vi.fn();
    render(<DataTable data={testData} columns={columns} sortable onSort={onSort} />);
    fireEvent.click(screen.getByText("Name"));
    expect(onSort).toHaveBeenCalledWith("name", "asc");
  });

  it("handles row selection", () => {
    const onSelectionChange = vi.fn();
    render(
      <DataTable data={testData} columns={columns} selectable onSelectionChange={onSelectionChange} />
    );
    fireEvent.click(screen.getByLabelText("Select row 1"));
    expect(onSelectionChange).toHaveBeenCalledWith([testData[0]]);
  });
});
```

**STOPPING POINT 2**: Your component is built. What next?

1. **Add keyboard navigation** - Arrow keys, Enter to select, Escape to deselect
2. **Add virtualization** - Handle thousands of rows without lag
3. **Add column resizing** - Draggable column borders
4. **Build more components** - Start another component from Step 1
5. **Set up Storybook** - Visual documentation and testing

---

## Workflow 2: Optimize Frontend Performance

### Step 1: Measure First

Never optimize without measuring. Run these diagnostics:

**Bundle analysis:**
```bash
# Vite
npx vite-bundle-visualizer

# Webpack
npx webpack-bundle-analyzer stats.json

# Next.js
ANALYZE=true next build
```

**Core Web Vitals targets:**

| Metric | Good | Needs Improvement | Poor |
|--------|------|-------------------|------|
| LCP (Largest Contentful Paint) | < 2.5s | 2.5-4.0s | > 4.0s |
| INP (Interaction to Next Paint) | < 200ms | 200-500ms | > 500ms |
| CLS (Cumulative Layout Shift) | < 0.1 | 0.1-0.25 | > 0.25 |

### Step 2: Common Fixes by Symptom

**Symptom: Large initial bundle (> 200KB gzipped)**

```typescript
// Before: Everything imported eagerly
import { HeavyChart } from "./HeavyChart";
import { AdminPanel } from "./AdminPanel";

// After: Lazy load routes and heavy components
const HeavyChart = lazy(() => import("./HeavyChart"));
const AdminPanel = lazy(() => import("./AdminPanel"));

function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <Routes>
        <Route path="/dashboard" element={<HeavyChart />} />
        <Route path="/admin" element={<AdminPanel />} />
      </Routes>
    </Suspense>
  );
}
```

**Symptom: Slow re-renders (UI feels sluggish when typing or scrolling)**

```typescript
// Before: Parent re-render causes all children to re-render
function ProductList({ products, filter }: Props) {
  const filtered = products.filter((p) => p.name.includes(filter));
  return filtered.map((p) => <ProductCard key={p.id} product={p} />);
}

// After: Memoize expensive computation and stabilize child props
function ProductList({ products, filter }: Props) {
  const filtered = useMemo(
    () => products.filter((p) => p.name.includes(filter)),
    [products, filter]
  );
  return filtered.map((p) => <ProductCard key={p.id} product={p} />);
}

const ProductCard = memo(function ProductCard({ product }: { product: Product }) {
  return <div>{product.name} - ${product.price}</div>;
});
```

**Symptom: Layout shifts (CLS > 0.1)**

```css
/* Reserve space for images and dynamic content */
.image-container {
  aspect-ratio: 16 / 9;
  width: 100%;
}

/* Reserve space for fonts */
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-display: swap;
  size-adjust: 107%;  /* Match fallback font metrics */
}
```

### Step 3: Performance Budget

Add these checks to your CI pipeline:

```json
// .lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "first-contentful-paint": ["warn", { "maxNumericValue": 1500 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-byte-weight": ["warn", { "maxNumericValue": 500000 }]
      }
    }
  }
}
```

**STOPPING POINT 3**: Performance is measured. What do you want to optimize?

1. **Reduce bundle size** - Tree shaking, code splitting, import analysis
2. **Speed up rendering** - Virtualization, memoization, concurrent features
3. **Improve loading experience** - Skeleton screens, streaming SSR, prefetching
4. **Set up continuous monitoring** - Lighthouse CI, Real User Monitoring (RUM)

---

## Workflow 3: Set Up a Frontend Project from Scratch

### Step 1: Choose Your Stack

**Decision tree:**

```
Building a marketing site or blog?
  -> Next.js (or Astro if mostly static)

Building a SPA dashboard or internal tool?
  -> Vite + React (or Vue)

Building a full-stack app with auth, DB, etc.?
  -> Next.js or Remix

Need SEO and fast first load?
  -> Next.js (SSR/SSG)

Pure client-side, no server rendering needed?
  -> Vite + React
```

### Step 2: Project Scaffolding

**Next.js project:**
```bash
npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir
```

**Vite + React project:**
```bash
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm install -D tailwindcss @tailwindcss/vite
```

### Step 3: Project Structure

```
src/
  app/                    # Next.js app router pages (or pages/ for Vite)
    layout.tsx
    page.tsx
    dashboard/
      page.tsx
  components/
    ui/                   # Generic, reusable components
      Button.tsx
      Input.tsx
      Modal.tsx
      index.ts            # Barrel export
    features/             # Feature-specific components
      auth/
        LoginForm.tsx
        SignupForm.tsx
      dashboard/
        MetricsCard.tsx
        ActivityFeed.tsx
  hooks/                  # Custom React hooks
    useDebounce.ts
    useLocalStorage.ts
    useMediaQuery.ts
  lib/                    # Utilities, API clients, constants
    api.ts                # Fetch wrapper / API client
    utils.ts              # Pure utility functions
    constants.ts
  types/                  # Shared TypeScript types
    index.ts
  styles/                 # Global styles
    globals.css
```

### Step 4: Essential Tooling Config

**ESLint:**
```json
// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react-hooks/recommended",
    "prettier"
  ],
  "rules": {
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "react-hooks/exhaustive-deps": "warn"
  }
}
```

**TypeScript strict mode:**
```json
// tsconfig.json (key settings)
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "forceConsistentCasingInFileNames": true
  }
}
```

**STOPPING POINT 4**: Your project is scaffolded. What next?

1. **Add a component library** - Set up shadcn/ui, Radix, or Headless UI
2. **Add testing** - Vitest + Testing Library + Playwright
3. **Add authentication** - NextAuth, Clerk, or custom auth flow
4. **Add API integration** - Fetch wrapper, React Query, or SWR
5. **Set up CI/CD** - GitHub Actions for lint, test, build, deploy

---

## Workflow 4: Implement Responsive Design

### Step 1: Define Breakpoints

Use a mobile-first approach. These breakpoints cover most devices:

```css
/* Tailwind defaults - good for most projects */
/* sm: 640px   - Large phones, landscape */
/* md: 768px   - Tablets */
/* lg: 1024px  - Laptops */
/* xl: 1280px  - Desktops */
/* 2xl: 1536px - Large monitors */
```

**Rule: Design for mobile first, then add complexity at larger sizes.**

### Step 2: Common Responsive Patterns

**Sidebar layout (collapses on mobile):**
```tsx
function AppLayout({ children }: { children: React.ReactNode }) {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  return (
    <div className="flex min-h-screen">
      {/* Mobile overlay */}
      {sidebarOpen && (
        <div
          className="fixed inset-0 bg-black/50 z-40 lg:hidden"
          onClick={() => setSidebarOpen(false)}
        />
      )}

      {/* Sidebar: off-screen on mobile, fixed on desktop */}
      <aside className={`
        fixed inset-y-0 left-0 z-50 w-64 bg-white border-r
        transform transition-transform duration-200
        ${sidebarOpen ? "translate-x-0" : "-translate-x-full"}
        lg:translate-x-0 lg:static lg:z-auto
      `}>
        <nav className="p-4">
          {/* Navigation items */}
        </nav>
      </aside>

      {/* Main content */}
      <main className="flex-1 min-w-0">
        <header className="lg:hidden p-4 border-b">
          <button onClick={() => setSidebarOpen(true)} aria-label="Open menu">
            Menu
          </button>
        </header>
        <div className="p-4 lg:p-8">
          {children}
        </div>
      </main>
    </div>
  );
}
```

**Responsive grid that reflows:**
```tsx
// 1 column on mobile, 2 on tablet, 3 on desktop
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  {items.map((item) => (
    <Card key={item.id} item={item} />
  ))}
</div>
```

**Responsive typography:**
```css
/* Use clamp() for fluid scaling between breakpoints */
h1 {
  font-size: clamp(1.5rem, 4vw, 3rem);
  line-height: 1.2;
}

p {
  font-size: clamp(0.875rem, 1.5vw, 1.125rem);
  line-height: 1.6;
  max-width: 65ch; /* Readable line length */
}
```

### Step 3: Testing Responsiveness

```typescript
// Playwright responsive tests
const viewports = [
  { name: "mobile", width: 375, height: 812 },
  { name: "tablet", width: 768, height: 1024 },
  { name: "desktop", width: 1440, height: 900 },
];

for (const vp of viewports) {
  test(`dashboard renders on ${vp.name}`, async ({ page }) => {
    await page.setViewportSize({ width: vp.width, height: vp.height });
    await page.goto("/dashboard");

    if (vp.name === "mobile") {
      await expect(page.getByLabel("Open menu")).toBeVisible();
      await expect(page.locator("aside")).not.toBeVisible();
    } else {
      await expect(page.locator("aside")).toBeVisible();
    }
  });
}
```

**STOPPING POINT 5**: Your responsive layout works. What next?

1. **Add touch interactions** - Swipe gestures, pull-to-refresh
2. **Optimize images** - Responsive images with srcset, next/image, AVIF/WebP
3. **Handle orientation changes** - Landscape vs portrait adjustments
4. **Test on real devices** - BrowserStack or physical device testing

---

## Workflow 5: Add State Management

### Step 1: Choose the Right Solution

**Decision tree -- start simple, escalate only when needed:**

```
Is the state local to one component?
  -> useState / useReducer. Done.

Is the state shared between 2-3 nearby components?
  -> Lift state up to common parent. Done.

Is the state shared across many components (theme, auth, locale)?
  -> React Context + useReducer. Done.

Is the state complex with many update patterns?
  -> Zustand (lightweight) or Redux Toolkit (full-featured)

Is the state server data (API responses)?
  -> React Query / SWR. NOT Redux.

Do you need real-time sync across tabs/windows?
  -> Zustand with persist middleware, or Jotai
```

### Step 2: Zustand (Recommended Default)

```typescript
// stores/useCartStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

interface CartItem {
  productId: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addItem: (item: Omit<CartItem, "quantity">) => void;
  removeItem: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
  totalPrice: () => number;
}

export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],

      addItem: (item) =>
        set((state) => {
          const existing = state.items.find((i) => i.productId === item.productId);
          if (existing) {
            return {
              items: state.items.map((i) =>
                i.productId === item.productId
                  ? { ...i, quantity: i.quantity + 1 }
                  : i
              ),
            };
          }
          return { items: [...state.items, { ...item, quantity: 1 }] };
        }),

      removeItem: (productId) =>
        set((state) => ({
          items: state.items.filter((i) => i.productId !== productId),
        })),

      updateQuantity: (productId, quantity) =>
        set((state) => ({
          items: quantity <= 0
            ? state.items.filter((i) => i.productId !== productId)
            : state.items.map((i) =>
                i.productId === productId ? { ...i, quantity } : i
              ),
        })),

      clearCart: () => set({ items: [] }),

      totalPrice: () =>
        get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    }),
    { name: "cart-storage" }
  )
);
```

**Usage in components:**
```tsx
function CartButton() {
  const itemCount = useCartStore((state) => state.items.length);
  return <button>Cart ({itemCount})</button>;
}

function CartTotal() {
  const totalPrice = useCartStore((state) => state.totalPrice());
  return <span>${totalPrice.toFixed(2)}</span>;
}
```

### Step 3: Server State with React Query

```typescript
// hooks/useOrders.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "@/lib/api";

export function useOrders(status?: string) {
  return useQuery({
    queryKey: ["orders", { status }],
    queryFn: () => api.get("/orders", { params: { status } }),
    staleTime: 30_000,       // Consider fresh for 30s
    refetchInterval: 60_000, // Auto-refresh every 60s
  });
}

export function useCreateOrder() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateOrderRequest) => api.post("/orders", data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["orders"] });
    },
  });
}

// Usage
function OrdersPage() {
  const { data: orders, isLoading, error } = useOrders("pending");
  const createOrder = useCreateOrder();

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorDisplay error={error} />;

  return (
    <div>
      <OrderList orders={orders} />
      <button
        onClick={() => createOrder.mutate(newOrderData)}
        disabled={createOrder.isPending}
      >
        {createOrder.isPending ? "Creating..." : "Create Order"}
      </button>
    </div>
  );
}
```

**STOPPING POINT 6**: State management is in place. What next?

1. **Add optimistic updates** - Update UI before server confirms
2. **Add offline support** - Persist React Query cache, queue mutations
3. **Add DevTools** - Zustand devtools, React Query devtools for debugging
4. **Add URL state** - Sync filters and pagination with URL search params
5. **Add real-time updates** - WebSocket or SSE for live data

