PickCSS
Back to Blog

Setting Up a Design System in Next.js

January 10, 202610 min readBy Brandon Watson

Next.js and Tailwind CSS are the most popular stack for modern React applications. Here's how to properly integrate design tokens into your Next.js project for a maintainable, scalable design system.

Project Structure for Design Tokens

A well-organized structure makes your design system easy to maintain:

your-nextjs-app/
├── src/
│   ├── app/
│   │   ├── globals.css       # CSS variables defined here
│   │   └── layout.tsx        # Import globals.css
│   ├── styles/
│   │   └── theme.css         # Optional: separate theme file
│   └── lib/
│       └── theme.ts          # TypeScript theme constants
├── tailwind.config.js        # Tailwind configuration
└── package.json

Where to Put globals.css

In Next.js App Router, global styles go in src/app/globals.css and are imported in layout.tsx:

/* src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  /* Colors */
  --color-background: #ffffff;
  --color-foreground: #0f172a;
  --color-primary: #3b82f6;
  --color-primary-foreground: #ffffff;
  --color-secondary: #64748b;
  --color-secondary-foreground: #ffffff;
  --color-muted: #f1f5f9;
  --color-muted-foreground: #64748b;
  --color-accent: #f59e0b;
  --color-accent-foreground: #ffffff;
  --color-border: #e2e8f0;

  /* Typography */
  --font-family: 'Inter', sans-serif;
  --font-family-heading: 'Inter', sans-serif;

  /* Spacing - use Tailwind's scale */
  --spacing-1: 0.25rem;
  --spacing-2: 0.5rem;
  --spacing-4: 1rem;
  --spacing-6: 1.5rem;
  --spacing-8: 2rem;

  /* Border Radius */
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;

  /* Shadows */
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}

:root.dark {
  --color-background: #0f172a;
  --color-foreground: #f8fafc;
  --color-primary: #60a5fa;
  --color-muted: #1e293b;
  --color-muted-foreground: #94a3b8;
  --color-border: #334155;
}

tailwind.config.js Setup

Extend Tailwind to use your CSS variables. This lets you use Tailwind classes that reference your tokens:

/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: 'class',
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        background: 'var(--color-background)',
        foreground: 'var(--color-foreground)',
        primary: {
          DEFAULT: 'var(--color-primary)',
          foreground: 'var(--color-primary-foreground)',
        },
        secondary: {
          DEFAULT: 'var(--color-secondary)',
          foreground: 'var(--color-secondary-foreground)',
        },
        muted: {
          DEFAULT: 'var(--color-muted)',
          foreground: 'var(--color-muted-foreground)',
        },
        accent: {
          DEFAULT: 'var(--color-accent)',
          foreground: 'var(--color-accent-foreground)',
        },
        border: 'var(--color-border)',
      },
      borderRadius: {
        sm: 'var(--radius-sm)',
        md: 'var(--radius-md)',
        lg: 'var(--radius-lg)',
      },
      fontFamily: {
        sans: ['var(--font-family)', 'sans-serif'],
        heading: ['var(--font-family-heading)', 'sans-serif'],
      },
      boxShadow: {
        sm: 'var(--shadow-sm)',
        md: 'var(--shadow-md)',
        lg: 'var(--shadow-lg)',
      },
    },
  },
  plugins: [],
}

App Router vs Pages Router

The setup differs slightly between Next.js routing paradigms:

App Router (Recommended)

// src/app/layout.tsx
import './globals.css';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Pages Router

// src/pages/_app.tsx
import '../styles/globals.css';
import type { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

TypeScript Integration

Create a TypeScript file for type-safe access to your theme values when needed in JavaScript:

// src/lib/theme.ts
export const theme = {
  colors: {
    primary: 'var(--color-primary)',
    secondary: 'var(--color-secondary)',
    background: 'var(--color-background)',
    foreground: 'var(--color-foreground)',
    muted: 'var(--color-muted)',
    accent: 'var(--color-accent)',
    border: 'var(--color-border)',
  },
  radius: {
    sm: 'var(--radius-sm)',
    md: 'var(--radius-md)',
    lg: 'var(--radius-lg)',
  },
  // Add more as needed
} as const;

export type ThemeColors = keyof typeof theme.colors;

Use it in components that need programmatic access:

import { theme } from '@/lib/theme';

// In a component
<div style={{ borderColor: theme.colors.border }}>
  Programmatic styling
</div>

shadcn/ui Integration

shadcn/ui uses the exact same CSS variable pattern. If you're using shadcn, your globals.css replaces theirs:

/* shadcn/ui expects these variable names */
:root {
  --background: 0 0% 100%;           /* HSL format */
  --foreground: 222.2 84% 4.9%;
  --primary: 221.2 83.2% 53.3%;
  --primary-foreground: 210 40% 98%;
  /* ... */
}

/* Or use hex with the naming convention PickCSS uses */
:root {
  --color-background: #ffffff;
  --color-foreground: #0f172a;
  --color-primary: #3b82f6;
  /* ... */
}

PickCSS exports both formats—choose the one that matches your project.

Using PickCSS Output in Next.js

When you export from PickCSS, you get files ready to drop into your Next.js project:

pickcss-export/
├── theme.css              # → src/app/globals.css (replace/merge)
├── globals.css            # → shadcn/ui compatible version
├── tailwind.config.js     # → merge with your tailwind.config.js
├── theme.ts               # → src/lib/theme.ts
└── tokens.json            # → for Figma/Storybook integration

Step-by-Step Integration

  1. Copy CSS variables from theme.css into your globals.css
  2. Merge Tailwind config - copy the extend section into your existing config
  3. Optional: Add theme.ts for TypeScript access
  4. Restart dev server to pick up Tailwind changes

Best Practices

Use CSS Variables in Tailwind Classes

{/* Good - uses your design tokens */}
<button className="bg-primary text-primary-foreground rounded-md">
  Click me
</button>

{/* Also good - explicit variable reference */}
<button className="bg-[var(--color-primary)] rounded-[var(--radius-md)]">
  Click me
</button>

{/* Avoid - hardcoded values */}
<button className="bg-blue-500 rounded-lg">
  Click me
</button>

Keep Variables in One Place

Define all variables in globals.css. Don't scatter them across component CSS files.

Use Semantic Names

--color-primary is better than --blue-500 because it describes purpose, not appearance.

Getting Started

Skip the manual setup. PickCSS generates all these files automatically based on your design preferences.

Create your Next.js design system in 5 minutes.

Put this into practice

Create a complete design system with production-ready tokens in minutes.

Start Building