Handbooks / Next.js / Chapter 6
Styling and Client State
7 pages · ~5 min✓ Reviewed
Builds on Server Actions and Route Handlers. Next up: Authentication Patterns.
Styling Mental Model: CSS Modules and Global CSS
1. CSS Modules
- File named *.module.css, imported as
styles - Class names are hashed per file, so they never collide
styleskeys are the class names; camelCase is easiest to access- Combine classes with a template string or
clsx
tsx
import styles from './x.module.css' export const Btn = () => ( <div className={styles.card}> <button className={`${styles.btn} ${styles.primary}`} /> </div> )
keys of styles match the class names
2. Selector Rules and Global CSS
- A module selector needs at least one local class
- Bare
h1 {}is not allowed: pair it with a class or use:global(h1) import './globals.css'only in rootapp/layout.tsx- Global holds resets, CSS variables, base typography for every route
3. Modules vs Global
| Modules | Global CSS | |
|---|---|---|
| Scope | Per component | Site-wide |
| Works in | Server and Client Components | Every route |
| Order | Independent | Order-dependent |
Keep global minimal
Site-wide and order-dependent: use Modules for component styles.
Tailwind Setup and Daily Usage
1. Install and Load
npm i tailwindcss @tailwindcss/postcss postcss- Add the plugin in
postcss.config.mjs - v4 loads with one line in
globals.css:@import "tailwindcss"; - No
tailwind.config.jsor three@tailwinddirectives by default
javascript
export default {
plugins: { '@tailwindcss/postcss': {} },
};postcss.config.mjs
2. Theme and Variants
- @theme in CSS adds tokens;
--color-brandgivesbg-brandandtext-brand - Variants stack:
hover:focus:md:dark:, e.g.md:flex dark:bg-zinc-900 - Plain CSS with no runtime, so it works in Server Components
css
@import "tailwindcss"; @theme { --color-brand: #0a7; }
globals.css
3. Static Class Strings
| Class string |
|---|
CSS-in-JS and Server Components
1. Why Runtime CSS-in-JS Breaks
- styled-components, Emotion inject styles via context and hooks
- So they work only in Client Components: add
'use client' - Server Components: no hooks, context or browser APIs
- Import one there: fails at build or render
2. SSR Needs a Style Registry
- Rendercollect styles
- useServerInsertedHTMLflush styles
- HTMLstyles arrive first
- Registry lives in a client provider wrapping
children - Without it: flash of unstyled content
3. Runtime vs Zero-runtime
Client State: useState, Context and Zustand
1. Local State and Context
useStateis local state, so the component needs'use client'- Siblings share it? Lift state up to the nearest common parent
- Context avoids prop drilling
- Provider is a Client Component in
layout.tsx, wrapping{children} - Server Components still pass through as
children
Context re-renders
Every consumer re-renders when the value changes. Split contexts by concern and memoise the value object.
2. Zustand
tsx
const useStore = create((set) => ({ count: 0, inc: () => set(s => ({ count: s.count + 1 })), })) const count = useStore(s => s.count)
selector: re-renders only when that slice changes
3. Zustand on the Server
- A module-level store is shared by all requests
- It leaks data between users
- Never read the store in Server Components
URL Search Params and Theming Without Flash
1. URL State vs useState
- URL state is shareable, bookmarkable, survives refresh and back/forward
- URL: filters, sort, page, tabs
- useState: modals, input drafts (ephemeral)
2. Reading and Writing Params
tsx
export default async function Page({ searchParams }) { const { q } = await searchParams }
server: read from the page prop
tsx
const sp = useSearchParams() const params = new URLSearchParams(sp) params.set('page', '2') router.replace(`?${params}`)
client: read-only, so copy then write
searchParamsis a Promise; using it makes the route dynamicpushadds a history entry;replacedoes notuseSearchParamswithout Suspense can bail out static rendering of the page
3. Theme Without Flash
- Server HTMLsaved theme unknown
- Inline script in headsets class on html
- First paintright colours
- HydratesuppressHydrationWarning
Styling and State Cheat Sheet
1. Styling picker
| Tool | Use for |
|---|---|
| CSS Modules | scoped component CSS |
| Tailwind | utility classes, no runtime |
| Global CSS | resets, variables; root layout only |
| Runtime CSS-in-JS | Client Components only |
2. State picker
| State | Pick |
|---|---|
| Local | useState |
| Shared subtree | context; rerenders all consumers |
| Global store | Zustand + selectors |
| Shareable, bookmarkable | URL params |
| Server-readable preference | cookie |
Check yourself
A component uses className={bg-${color}-500} with color set to 'red'. The element has no background. Why?
- Tailwind scans source for complete class strings;
bg-red-500never appears, so no CSS is generated - Fix: map to literal names, e.g.
{red: 'bg-red-500'}
A module-level Zustand store holds the logged-in user. Two users hit the server together. What goes wrong?
- The module is created once per server process, so all requests share one store
- One user's data can leak to another
- Fix: create the store per request in a provider
The saved theme is dark, but each page load flashes light first. Why, and what fixes it?
- The server cannot read localStorage, so it renders the default theme
- Fix: a blocking inline script in
<head>sets the theme on<html>before first paint - Add
suppressHydrationWarningon<html>