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:
<Card title="..." subtitle="..." imageUrl="..." showButton buttonText="..." onClick={...} variant="primary" />
Good — compose smaller components:
<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
<!-- 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:
<!-- 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:
<button class="transition-all duration-200 hover:scale-105 hover:shadow-lg">
For complex orchestration (lists, page transitions, gestures), use Framer Motion:
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:
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
Use the next-themes package to toggle and persist:
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:
// 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):
export const dynamic = "force-dynamic";
Revalidate on demand — after a mutation, kick a specific path to regenerate immediately instead of waiting for the timer:
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+revalidatePathafter 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:
"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:
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)
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:
export default {
images: {
remotePatterns: [
{ protocol: "https", hostname: "*.supabase.co" },
{ protocol: "https", hostname: "images.unsplash.com" },
],
},
};
Rules:
- Always specify
width+height(or usefillwith a sized parent) - Add
priorityto above-the-fold images (hero, navbar logo) - Use
sizeswhenever you usefill— without it, Next loads the largest size - Use
alton 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:
@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:
<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 avariantprop, NOT 47 separate componentsCard.tsx— composable, like shadcn (<Card><CardHeader><CardTitle>)Input.tsx,Label.tsx,Textarea.tsx— form primitivesModal.tsx— using<dialog>element or Radix DialogToast.tsx— small wrapper aroundsonner
Step 3 — Lock in one visual language and apply it everywhere:
- One shadow scale (
shadow-mdeverywhere, never mix sizes) - One corner radius (
rounded-2xlfor cards,rounded-fullfor pills) - One spacing rhythm (sections
py-20, cardsp-6, gapsgap-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 —
sonnerpackage, 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.tsxwith 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.memoanduseMemowhere needed. - Reaching for state libraries too early.
useStatehandles 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
