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.

U

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.

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:

tsx
// 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>
  );
}
css
/* 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:

tsx
// 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`:

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);
}