Best UI Patterns for Next.js Applications
Next.js has its own conventions for organizing UI components. Learn the patterns that scale — from Server vs Client Components to CSS Modules, and how to build a robust component architecture for your Next.js 13+ app.
UIXplor Team
March 11, 2026 · 10 min read
01Next.js App Router Component Architecture
Next.js 13+ introduces the App Router with a fundamental shift: components are Server Components by default. This changes how you think about component organization.
Server Components: - Render on the server — zero JavaScript bundle cost - Can directly access databases, file systems, API keys - Cannot use hooks (`useState`, `useEffect`) or browser APIs - Best for: layouts, navigation, data-heavy content
Client Components (add `'use client'` directive): - Run in the browser — interactive, respond to events - Required for: hover states, click handlers, form inputs, animations - Best for: buttons, modals, dropdowns, anything interactive
The golden rule: push `'use client'` as far down the tree as possible. A page with a single interactive button shouldn't make the whole page a Client Component.
02Recommended Folder Structure
app/
layout.tsx ← Root layout (Server Component)
page.tsx ← Home page (Server Component)
components/
ui/ ← Reusable UI primitives
Button.tsx
Button.module.css
Card.tsx
Card.module.css
layout/ ← Layout components
Header.tsx
Footer.tsx
Sidebar.tsx
features/ ← Feature-specific components
auth/
LoginForm.tsx (Client Component)
AuthProvider.tsx (Client Component)
dashboard/
StatsGrid.tsx
ActivityFeed.tsx
lib/
utils.ts ← Utility functions
db.ts ← Database clientThe `ui/` directory holds your design system primitives. The `features/` directory holds business-logic-heavy components.
03CSS Modules in Next.js
Next.js has first-class support for CSS Modules. They scope class names locally so you never have naming collisions:
// components/ui/Button.tsx
'use client';
import styles from './Button.module.css';
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'ghost' | 'destructive';
size?: 'sm' | 'md' | 'lg';
onClick?: () => void;
}
export function Button({ children, variant = 'primary', size = 'md', onClick }: ButtonProps) {
return (
<button
className={`${styles.btn} ${styles[`btn--${variant}`]} ${styles[`btn--${size}`]}`}
onClick={onClick}
>
{children}
</button>
);
}/* Button.module.css */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.btn--primary { background: #6C63FF; color: #fff; }
.btn--ghost { background: transparent; border: 1px solid #2A2A2A; color: rgba(255,255,255,0.7); }
.btn--sm { padding: 6px 14px; font-size: 12px; }
.btn--md { padding: 10px 20px; font-size: 14px; }
.btn--lg { padding: 14px 28px; font-size: 16px; }04Server Components for Data-Heavy UIs
One of the biggest wins in Next.js App Router is rendering data-heavy components on the server. Here's a product card that fetches its own data:
// components/features/ProductCard.tsx (Server Component — no 'use client')
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 3600 } // Cache for 1 hour
});
return res.json();
}
export async function ProductCard({ productId }: { productId: string }) {
const product = await getProduct(productId);
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">${product.price}</p>
{/* Note: AddToCart must be a Client Component for the onClick */}
<AddToCart productId={productId} />
</div>
);
}The server streams this component with the product data pre-rendered — zero loading spinners, zero client-side data fetching, zero exposed API keys.
05Theme and Design Tokens with CSS Variables
Define your design tokens as CSS custom properties in `globals.css`:
/* app/globals.css */
:root {
/* Colors */
--color-bg: #0D0D0D;
--color-surface: #151515;
--color-border: #2A2A2A;
--color-accent: #6C63FF;
--color-text: rgba(255, 255, 255, 0.85);
--color-muted: rgba(255, 255, 255, 0.4);
/* Spacing */
--space-1: 4px;
--space-2: 8px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
/* Typography */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'Fira Code', 'JetBrains Mono', monospace;
/* Radius */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--radius-xl: 24px;
/* Shadows */
--shadow-sm: 0 2px 8px rgba(0,0,0,0.4);
--shadow-md: 0 8px 24px rgba(0,0,0,0.5);
--shadow-glow: 0 0 24px rgba(108,99,255,0.3);
}Related UI Components
Related Articles