# When to use these instructions

Use this skill whenever the user is building UI components, working with React/Next.js, applying Tailwind classes, making things responsive, or adding animations. Apply the opinionated stack and component patterns.

---

# Frontend

Building user interfaces with the modern web stack. This skill covers the
full frontend domain — components, layouts, styling, responsive design,
animations, and dark mode.

## When to use this
- The user is building any UI component (button, form, card, modal, etc.).
- The user is working in React, Next.js, Vue, or any modern framework.
- The user is applying Tailwind classes or making layouts responsive.
- The user is adding animations or interactivity.

## Stack-aware

This skill works for two common Next.js paths:
- **Stack A (managed):** Vercel + Clerk + Drizzle/Prisma + shadcn/ui
- **Stack B (Supabase-first):** Render or Vercel + Supabase Auth + raw `@supabase/supabase-js` + custom components

Both ship great products. The advice below applies to both unless a section is explicitly marked **[Stack A]** or **[Stack B]**.

## The opinionated stack (2026)

Recommend this stack for any new project — only deviate with a specific reason:

| Layer | Pick | Why |
|---|---|---|
| Framework | **Next.js 15+ (App Router)** | Most jobs, most docs, best AI training data |
| Language | **TypeScript** | Catches errors before they ship |
| Styling | **Tailwind CSS v4** | Every AI tool generates flawless Tailwind |
| Components | **shadcn/ui** | Copy-paste, fully owned, accessible by default |
| Icons | **lucide-react** | Free, clean, comprehensive |
| Fonts | **next/font/google** | Self-hosted, zero layout shift |
| Animation | **Framer Motion** + Tailwind transitions | Best DX for spring physics |
| Forms | **React Hook Form + Zod** | The standard (see Forms skill) |
| State (local) | **useState / useReducer** | Don't reach for Zustand/Redux until you have a real reason |
| State (server) | **TanStack Query** or **SWR** | Caching, revalidation, mutations |
| Notifications | **sonner** | Tiny, beautiful toasts |

For non-Next.js projects:
- **Vue** → Nuxt 3 + Tailwind + shadcn-vue
- **Svelte** → SvelteKit + Tailwind + shadcn-svelte
- **Solid** → SolidStart + Tailwind

## File structure

```
src/
  app/                    # App Router routes
    layout.tsx            # Root layout
    page.tsx              # Home page
    (marketing)/          # Route group for marketing pages
      pricing/page.tsx
      about/page.tsx
    dashboard/
      layout.tsx          # Dashboard layout
      page.tsx
  components/             # Reusable components
    ui/                   # shadcn/ui primitives
    Navbar.tsx
    Hero.tsx
  lib/                    # Utility functions
    utils.ts
    db.ts
  hooks/                  # Custom React hooks
  data/                   # Static data files
```

Group related code into folders. Components used in only one page can live
inside that page's folder.

## Component composition

The single most important React skill: **compose, don't customize**.

Bad — one component with many props:
```tsx
<Card title="..." subtitle="..." imageUrl="..." showButton buttonText="..." onClick={...} variant="primary" />
```

Good — compose smaller components:
```tsx
<Card>
  <CardImage src="..." />
  <CardHeader>
    <CardTitle>...</CardTitle>
    <CardDescription>...</CardDescription>
  </CardHeader>
  <CardFooter>
    <Button onClick={...}>...</Button>
  </CardFooter>
</Card>
```

This is the shadcn/ui pattern. Adopt it for everything.

## Server vs client components (Next.js App Router)

This is the most common confusion. Rules:

- **Default to Server Components.** They're faster, smaller, and run on the server.
- **Add `"use client"` ONLY when you need:**
  - `useState`, `useEffect`, or any hook
  - Event handlers (`onClick`, `onChange`)
  - Browser APIs (`localStorage`, `window`)
- **Pass server data down as props** to client components — don't fetch in client components.
- **Server Components can import client components**, but client components can't import server components (they can render them as children, though).

Common pattern: a server component fetches data, then passes it to a client component for interactivity.

## Tailwind power moves

```html
<!-- Gradient text -->
<h1 class="bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">

<!-- Glass-morphism card -->
<div class="bg-white/80 backdrop-blur-xl border border-white/20 rounded-2xl shadow-lg">

<!-- Hover lift -->
<div class="transition-all hover:-translate-y-1 hover:shadow-xl">

<!-- Sticky header with blur -->
<header class="sticky top-0 z-50 backdrop-blur-xl bg-white/70 border-b border-gray-200/50">

<!-- Centered narrow column -->
<div class="max-w-3xl mx-auto px-6">

<!-- Responsive grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
```

## Responsive design

**Always mobile-first.** Default classes target mobile, prefixed classes target larger:

```html
<!-- 1 col mobile, 2 col tablet, 3 col desktop -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">

<!-- Hidden on mobile, shown desktop -->
<div class="hidden md:block">

<!-- Stack on mobile, row on desktop -->
<div class="flex flex-col md:flex-row">

<!-- Smaller text on mobile -->
<h1 class="text-3xl md:text-5xl">
```

Breakpoints: `sm:` 640px, `md:` 768px, `lg:` 1024px, `xl:` 1280px.

## Animations

For 90% of cases, use Tailwind transitions:
```html
<button class="transition-all duration-200 hover:scale-105 hover:shadow-lg">
```

For complex orchestration (lists, page transitions, gestures), use Framer Motion:
```tsx
import { motion } from "framer-motion";

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.3 }}
>
```

For scroll-triggered animations, use the Intersection Observer API or
`framer-motion`'s `whileInView`.

## Dark mode

Tailwind's dark mode uses the `dark:` prefix:
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
```

Use the `next-themes` package to toggle and persist:
```bash
npm install next-themes
```

Wrap your app in `<ThemeProvider attribute="class" defaultTheme="system">`.

## ISR + revalidate (the most-missed Next.js pattern)

By default, App Router pages are either fully static (built once) or fully
dynamic (rendered on every request). **ISR (Incremental Static Regeneration)
is the middle ground**: serve a cached page for N seconds, then regenerate
in the background. This is how content sites stay fast AND fresh.

**Page-level ISR** — add this to any `page.tsx` or `layout.tsx`:
```tsx
// Cache this page for 30 minutes, then regenerate in the background
export const revalidate = 1800;
```

**Force fully dynamic** — for pages that must run on every request (admin,
auth-protected, real-time data):
```tsx
export const dynamic = "force-dynamic";
```

**Revalidate on demand** — after a mutation, kick a specific path to
regenerate immediately instead of waiting for the timer:
```tsx
import { revalidatePath, revalidateTag } from "next/cache";

// In a server action after saving a product
await db.update(products).set({ name }).where(eq(products.id, id));
revalidatePath("/products");           // regenerate the listing
revalidatePath(`/products/${slug}`);   // regenerate the detail page
revalidatePath("/");                   // regenerate the homepage
```

**When to use what:**
- Marketing site, blog, listings → `revalidate = 1800` (30 min)
- Product catalog with admin updates → `revalidate = 1800` + `revalidatePath` after admin save
- Dashboard, account pages, anything personalized → `dynamic = "force-dynamic"`
- Pure marketing pages that never change → no ISR (fully static)

## error.tsx and loading.tsx (mandatory in App Router)

Every route in App Router can have a sibling `error.tsx` and `loading.tsx`
that Next.js wires up automatically. **Add both to every route segment.**
Without them, errors crash the whole app and slow data shows a blank page.

**`error.tsx`** — graceful error boundary for the route:
```tsx
"use client";
import { useEffect } from "react";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    console.error(error);
  }, [error]);

  return (
    <div className="text-center py-20">
      <h2 className="text-2xl font-bold mb-2">Something went wrong</h2>
      <p className="text-muted mb-4">{error.message}</p>
      <button
        onClick={reset}
        className="px-4 py-2 bg-purple text-white rounded-lg"
      >
        Try again
      </button>
    </div>
  );
}
```

**`loading.tsx`** — skeleton shown while the route's server data loads:
```tsx
export default function Loading() {
  return (
    <div className="max-w-4xl mx-auto p-8 space-y-4">
      <div className="h-8 w-2/3 bg-surface-alt rounded-lg animate-pulse" />
      <div className="h-4 w-full bg-surface-alt rounded animate-pulse" />
      <div className="h-4 w-5/6 bg-surface-alt rounded animate-pulse" />
      <div className="grid grid-cols-3 gap-4 mt-8">
        {[1, 2, 3].map((i) => (
          <div key={i} className="h-40 bg-surface-alt rounded-2xl animate-pulse" />
        ))}
      </div>
    </div>
  );
}
```

These files automatically wrap the route in React Suspense and Error
Boundaries — no need to import anything or wire it up.

**Layered**: each nested route can have its own `error.tsx` and
`loading.tsx`. Errors bubble up to the nearest boundary.

## Images with next/image

The single biggest performance lever in any Next.js app. **Always use
`next/image`, never plain `<img>` tags.** It handles:
- Automatic WebP / AVIF conversion
- Responsive sizes (one image, multiple resolutions)
- Lazy loading by default (above-the-fold images opt out with `priority`)
- Layout shift prevention (you must specify width/height)
- Blur placeholders (low-res preview while the real image loads)

```tsx
import Image from "next/image";

// Local image (in /public)
<Image
  src="/hero.jpg"
  alt="Product hero"
  width={1200}
  height={800}
  priority  // above the fold — load immediately, no lazy
  className="rounded-2xl"
/>

// Remote image with blur placeholder
<Image
  src="https://supabase.co/storage/v1/object/public/products/lipstick.jpg"
  alt="Lipstick"
  width={400}
  height={400}
  placeholder="blur"
  blurDataURL="data:image/svg+xml;base64,..."
  className="object-cover"
/>

// Responsive — fills the parent and serves the right resolution per breakpoint
<div className="relative aspect-square">
  <Image
    src={product.imageUrl}
    alt={product.name}
    fill
    sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
    className="object-cover"
  />
</div>
```

**For remote images**, whitelist the domain in `next.config.ts`:
```ts
export default {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "*.supabase.co" },
      { protocol: "https", hostname: "images.unsplash.com" },
    ],
  },
};
```

**Rules:**
- Always specify `width` + `height` (or use `fill` with a sized parent)
- Add `priority` to above-the-fold images (hero, navbar logo)
- Use `sizes` whenever you use `fill` — without it, Next loads the largest size
- Use `alt` on every image — accessibility AND SEO

## Custom design systems (when you DON'T use shadcn)

shadcn is a great default but it isn't the only option. If you want a
distinctive look, build your own components using Tailwind utility classes
and CSS variables. Both reference projects in the codebooks.ai universe
(this site itself, and chokchok) ship custom components.

**Step 1 — Define your design tokens** in `src/app/globals.css`:
```css
@theme {
  --color-cream: #FFFAF4;
  --color-ink: #1A1814;
  --color-ink-light: #4A4740;
  --color-pink: #FF85C1;
  --color-yellow: #FCCA36;
  --color-surface: #EFEBE4;

  --font-display: "Syne", sans-serif;
  --font-body: "Space Grotesk", sans-serif;
}
```

Tailwind v4 picks these up automatically as utility classes:
```html
<h1 class="font-display text-ink bg-cream">Hello</h1>
```

**Step 2 — Build a small set of base components** in `src/components/ui/`:
- `Button.tsx` — variants via a `variant` prop, NOT 47 separate components
- `Card.tsx` — composable, like shadcn (`<Card><CardHeader><CardTitle>`)
- `Input.tsx`, `Label.tsx`, `Textarea.tsx` — form primitives
- `Modal.tsx` — using `<dialog>` element or Radix Dialog
- `Toast.tsx` — small wrapper around `sonner`

**Step 3 — Lock in one visual language** and apply it everywhere:
- One shadow scale (`shadow-md` everywhere, never mix sizes)
- One corner radius (`rounded-2xl` for cards, `rounded-full` for pills)
- One spacing rhythm (sections `py-20`, cards `p-6`, gaps `gap-6`)

**Step 4 — Document the palette** in a `/styleguide` page (visible only in
dev) or a comment in `globals.css`. Future-you will thank present-you.

When this beats shadcn:
- You want a brand-distinctive look (brutalist, editorial, soft pastel)
- You're allergic to dependencies and want to own the code
- You want the bundle to be tiny

When shadcn beats this:
- You want to ship in a day with sensible defaults
- You need accessible primitives (Radix) without writing them yourself
- Your project is a SaaS dashboard where consistency > distinctiveness

## Common patterns to know

- **Modal** — shadcn `<Dialog>`, native `<dialog>` element, or Radix Dialog
- **Drawer** — shadcn `<Sheet>` (slide-in panel)
- **Toast** — `sonner` package, fire from event handlers
- **Accordion** — shadcn `<Accordion>`, single or multiple expand
- **Combobox** — shadcn `<Command>`, searchable dropdown
- **Date picker** — shadcn `<Calendar>` + `<Popover>`
- **Data table** — shadcn `<Table>` + TanStack Table for sorting/filtering
- **Skeleton loader** — `loading.tsx` with animated divs (no library needed)

## Common mistakes to avoid

- **Adding "use client" to everything.** Server components are the default for a reason.
- **Fixed widths.** Use `max-w-*` so things shrink on small screens.
- **Inline styles.** Use Tailwind classes — they're tree-shaken and consistent.
- **Importing everything as a default export.** Use named exports for components.
- **Not memoizing expensive lists.** Use `React.memo` and `useMemo` where needed.
- **Reaching for state libraries too early.** `useState` handles 95% of cases.
- **Forgetting key props on lists.** React will warn you — listen to it.
- **Hardcoding hex colors.** Define them as Tailwind theme tokens or CSS variables.

## Going deeper
- Frontend: https://www.codebooks.ai/frontend
- UI Elements: https://www.codebooks.ai/ui-elements
- Responsive Design: https://www.codebooks.ai/responsive
- Animations: https://www.codebooks.ai/animations
- Dark Mode: https://www.codebooks.ai/dark-mode
- Project Structure: https://www.codebooks.ai/project-structure


---

This skill is part of the **CodeBooks Vibe Coding Skills Library**.
Browse all skills, install guides, and the source chapters at
https://www.codebooks.ai/skills
