CSSTailwind Configuration & Theming

Tailwind Configuration & Theming

Tailwind's utility classes aren't arbitrary — every spacing value, color, breakpoint, and font size comes from a design token table called the theme. The theme is what turns p-4 into 1rem of padding or text-blue-600 into a specific shade of blue, and it is fully customizable per project. Understanding how to configure it is the difference between a Tailwind site that looks like every other Tailwind site and one with its own visual identity.
tailwind.config.js
In Tailwind v3 and earlier, the theme lives in a JavaScript config file at the project root. It exports an object with a theme key, and everything inside theme.extend is merged on top of Tailwind's default design tokens rather than replacing them:

JS
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#eef6ff',
          100: '#d9eaff',
          500: '#2f7ff0',
          600: '#1c63d1',
          900: '#122a52',
        },
      },
      spacing: {
        18: '4.5rem',
        112: '28rem',
      },
      fontFamily: {
        display: ['Sora', 'ui-sans-serif', 'system-ui'],
      },
      screens: {
        xs: '480px',
      },
    },
  },
  plugins: [],
}
With this config, bg-brand-500, p-18, font-display, and the new xs: breakpoint prefix all become valid utility classes, alongside every default Tailwind utility that was already there.
extend vs. overriding the default theme
Where you place a key matters a great deal. Tailwind treats theme.extend as additive, but a key placed directly under theme (outside extend) completely replaces that entire token category:

Placement

Effect

When to use it

theme.extend.colors

Adds new colors alongside the full default palette (red-500, blue-600, etc. still work)

Almost always — you rarely want to lose the default palette entirely

theme.colors

Replaces the ENTIRE default color palette with only what you list

Design systems that want a small, closed, fully custom palette (no stray red-500 usages)

theme.extend.spacing

Adds new spacing values on top of the default 4px-based scale

Adding a few one-off sizes

theme.spacing

Replaces the entire spacing scale

Rare — usually only for a from-scratch design system with its own scale

Overriding theme.colors is easy to do by accident
A common mistake is copy-pasting a color object under theme instead of theme.extend, which silently deletes every built-in color. Classes like text-gray-500 or bg-red-500 that were working a moment ago will simply stop generating any CSS, with no error — the class just won't exist in the output. If utilities you expect suddenly don't apply, check whether the corresponding theme key was placed outside extend.
A worked example: a custom brand palette
Say a design team hands you a single brand blue and asks for a full tonal range around it. You'd generate (or pick from a tool like Tailwind's own color generator) a 50–900 scale and drop it into extend.colors under a semantic name, rather than a raw color name:

JS
theme: {
  extend: {
    colors: {
      brand: {
        50:  '#f0f7ff',
        100: '#dbeafe',
        200: '#bfdbfe',
        300: '#93c5fd',
        400: '#60a5fa',
        500: '#3b82f6', // the "base" brand color
        600: '#2563eb',
        700: '#1d4ed8',
        800: '#1e40af',
        900: '#1e3a8a',
      },
    },
  },
},
Now bg-brand-600, text-brand-50, and border-brand-300 are all available, and — crucially — if the brand color ever changes, updating it happens in exactly one place instead of hunting through every component for a hardcoded hex value.
Custom utilities via plugins
For one-off utility classes that don't fit the theme-token model (a text-shadow utility, a scrollbar-hiding utility, and so on), Tailwind exposes a small plugin API through addUtilities:

JS
const plugin = require('tailwindcss/plugin')

module.exports = {
  // ...
  plugins: [
    plugin(({ addUtilities }) => {
      addUtilities({
        '.text-shadow-sm': {
          textShadow: '0 1px 2px rgba(0, 0, 0, 0.2)',
        },
        '.scrollbar-hide': {
          scrollbarWidth: 'none',
          '&::-webkit-scrollbar': { display: 'none' },
        },
      })
    }),
  ],
}
  • For anything beyond a handful of utilities, reach for an official first-party plugin first — @tailwindcss/forms, @tailwindcss/typography, and @tailwindcss/container-queries cover the most common needs.

  • Plugins can also read from and extend the theme, so a plugin-provided utility can still respect your custom spacing or color scale.

Tailwind v4: CSS-first configuration
Configuration is moving into CSS itself
Tailwind v4 introduces a CSS-first configuration model where design tokens are declared directly in a stylesheet using an @theme block, instead of (or alongside) a JavaScript config file:

CSS
@import "tailwindcss";

@theme {
  --color-brand-500: #3b82f6;
  --color-brand-600: #2563eb;
  --spacing-18: 4.5rem;
  --font-display: "Sora", sans-serif;
}
Under this model, theme values are plain CSS custom properties, which means they're usable directly in hand-written CSS too (color: var(--color-brand-600)), not just through generated utility classes — a nice side benefit of moving configuration into CSS rather than a separate JS object. If you're starting a new project, check which major version your toolchain targets before deciding whether to reach for tailwind.config.js or an @theme block, since the two approaches (and their exact capabilities) differ by version.
Keep the config as the single source of design truth
Whichever version you use, resist the urge to sprinkle one-off arbitrary values like w-[327px] everywhere. Arbitrary values are a useful escape hatch, but a config (or @theme block) that captures your real spacing/color/type scale keeps utility classes meaningful and consistent across the whole codebase.