---
title: "Fix Dynamic Tailwind Classes with Class Registries"
slug: "class-registries-tailwind-cms"
published: "2026-01-20"
updated: "2026-04-06"
categories:
  - "Payload"
tags:
  - "dynamic Tailwind classes"
  - "class registry"
  - "Tailwind CSS"
  - "CMS styling"
  - "Payload CMS"
  - "static class mappings"
  - "TypeScript registries"
  - "shadcn/ui"
  - "build-time generation"
  - "runtime styling issues"
llm-intent: "how-to"
audience-level: "intermediate"
llm-purpose: "Dynamic Tailwind classes break at runtime—use class registries to map CMS values to prebuilt Tailwind classes for reliable, type-safe styling. Read the…"
llm-prereqs:
  - "Tailwind CSS"
  - "TypeScript"
  - "shadcn/ui"
  - "Payload CMS"
  - "React"
---

**Summary Triples**
- (Fix Dynamic Tailwind Classes with Class Registries, expresses-intent, how-to)
- (Fix Dynamic Tailwind Classes with Class Registries, covers-topic, dynamic Tailwind classes)
- (Fix Dynamic Tailwind Classes with Class Registries, provides-guidance-for, Dynamic Tailwind classes break at runtime—use class registries to map CMS values to prebuilt Tailwind classes for reliable, type-safe styling. Read the…)

### {GOAL}
Dynamic Tailwind classes break at runtime—use class registries to map CMS values to prebuilt Tailwind classes for reliable, type-safe styling. Read the…

### {PREREQS}
- Tailwind CSS
- TypeScript
- shadcn/ui
- Payload CMS
- React

### {STEPS}
1. Create a static registry file
2. Export a helper lookup function
3. Consume registry in components
4. Extend registries for other tokens
5. Validate and maintain registries

<!-- llm:goal="Dynamic Tailwind classes break at runtime—use class registries to map CMS values to prebuilt Tailwind classes for reliable, type-safe styling. Read the…" -->
<!-- llm:prereq="Tailwind CSS" -->
<!-- llm:prereq="TypeScript" -->
<!-- llm:prereq="shadcn/ui" -->
<!-- llm:prereq="Payload CMS" -->
<!-- llm:prereq="React" -->

# Fix Dynamic Tailwind Classes with Class Registries
> Dynamic Tailwind classes break at runtime—use class registries to map CMS values to prebuilt Tailwind classes for reliable, type-safe styling. Read the…
Matija Žiberna · 2026-01-20

I was building a page layout system for a Payload CMS-driven project when I hit a wall. The navbar height was configurable in the CMS, and I needed to apply the correct top padding to page content. My first instinct was to generate a Tailwind class dynamically—something like dynamic class names with template variables. After hours of debugging cryptic CSS parsing errors, I discovered the real problem: Tailwind can't generate classes at runtime because it builds your CSS at development time, not in the browser.

The solution? **Class registries**—a pattern used by shadcn/ui internally. Instead of generating classes dynamically, you map static CMS values to predefined Tailwind classes at development time. This guide walks you through exactly how to implement this approach.

## Why Dynamic Tailwind Classes Fail

Tailwind CSS works by scanning your source code at build time, finding class names like `pt-20` or `bg-primary`, and generating the corresponding CSS. It does this once, during the build process. The browser never sees raw class names—only the final compiled CSS.

When you try to create a class dynamically—for instance, interpolating a CMS value into a class name—Tailwind has no way to detect it during the build. The dynamically generated class name doesn't match any static class pattern, so Tailwind never generates the CSS. Your styles don't apply, and you're left with an element that has no padding.

Even worse, some setups report a parsing error when they encounter invalid arbitrary values like arbitrary brackets with dynamic content. The CSS parser sees a class name that looks valid but contains invalid CSS syntax, causing the entire build to fail.

The root issue: **CMS flexibility and Tailwind's build-time generation are fundamentally at odds.** You can't bridge this gap by trying to be clever with string interpolation. You need a different approach.

## The Class Registry Pattern

A class registry is simple: a static object that maps CMS values to Tailwind classes. It's created at development time when you write your code, so Tailwind can see and process every class name. When your code runs, you look up the appropriate class based on the CMS value, and Tailwind has already generated the CSS for it.

This is exactly how shadcn/ui handles variants, colors, and spacing. It's not a workaround—it's the intended way to handle dynamic styling in a Tailwind project.

The pattern consists of three parts:

1. **A static registry object** that maps values to classes
2. **A helper function** that looks up the class for a given value
3. **A component or utility** that uses the helper function

Let's implement this step by step.

## Step 1: Create Your First Class Registry

Start with a simple example: mapping navbar heights to padding classes.

Create a new file:

```typescript
// File: src/lib/navbar-classes.ts

/**
 * Navbar Height to Tailwind Padding Class Mappings
 *
 * Maps numeric navbar heights (in pixels) to corresponding Tailwind padding-top classes.
 * This allows the PageLayout component to apply the correct spacing without generating
 * dynamic class names at runtime.
 *
 * Pattern: navbar height in pixels → Tailwind pt- class
 */

export const NAVBAR_PADDING_CLASSES: Record<number, string> = {
  64: "pt-16", // 64px (4rem)
  80: "pt-20", // 80px (5rem)
  96: "pt-24", // 96px (6rem)
  112: "pt-28", // 112px (7rem)
  128: "pt-32", // 128px (8rem)
};

/**
 * Get Tailwind padding-top class for navbar height
 *
 * @param heightPx - Navbar height in pixels
 * @param fallback - Default class if height not found (defaults to 'pt-20')
 * @returns Tailwind padding class ready to use in className
 */
export function getNavbarPaddingClass(
  heightPx: number,
  fallback = "pt-20",
): string {
  return NAVBAR_PADDING_CLASSES[heightPx] || fallback;
}
```

This file does two things. First, it defines `NAVBAR_PADDING_CLASSES` as a static mapping. Every key-value pair in this object is a Tailwind class name that Tailwind will see during the build and generate CSS for. The keys are navbar heights in pixels, and the values are the corresponding `pt-` (padding-top) classes.

Second, it exports a helper function `getNavbarPaddingClass()` that takes a navbar height and returns the appropriate class name. The fallback ensures that if a height isn't in the registry, you get a sensible default instead of `undefined`.

## Step 2: Use the Registry in Your Component

Now create a component that uses this registry:

```typescript
// File: src/components/page-layout.tsx

import { getNavbarPaddingClass } from '@/lib/navbar-classes';

interface PageLayoutProps {
  children: React.ReactNode;
  navbarHeight: number;
  isNavbarFixed: boolean;
}

export async function PageLayout({
  children,
  navbarHeight,
  isNavbarFixed,
}: PageLayoutProps) {
  // Get the class name based on the navbar height
  const paddingClass = isNavbarFixed ? getNavbarPaddingClass(navbarHeight) : '';

  return (
    <div className={paddingClass}>
      {children}
    </div>
  );
}
```

The component receives `navbarHeight` and `isNavbarFixed` as props. If the navbar is fixed, it calls `getNavbarPaddingClass()` to get the appropriate class name, then applies it via `className`. If the navbar is relative (not fixed), it uses an empty string and no padding is applied.

Here's why this works: when Tailwind scans your source code, it finds the static class names `pt-16`, `pt-20`, `pt-24`, etc. in the `navbar-classes.ts` file. It generates CSS for all of them. Later, at runtime, your component picks the right class name based on the CMS data and applies it to the element. Tailwind's CSS is already there, waiting.

## Step 3: Extend to Other Styles

Now that you understand the pattern, extend it to other CMS values. Here's an example for colors:

```typescript
// File: src/lib/color-classes.ts

/**
 * Color to Tailwind Class Mappings
 *
 * Maps semantic color names (used in your CMS) to actual Tailwind color classes.
 * This is the same pattern shadcn/ui uses for its design system.
 */

export const TEXT_COLOR_CLASSES: Record<string, string> = {
  white: "text-white",
  light: "text-gray-100",
  muted: "text-gray-600",
  primary: "text-blue-600",
  dark: "text-gray-900",
  accent: "text-orange-500",
};

export const BG_COLOR_CLASSES: Record<string, string> = {
  white: "bg-white",
  light: "bg-gray-50",
  muted: "bg-gray-100",
  primary: "bg-blue-600",
  dark: "bg-gray-900",
  accent: "bg-orange-500",
};

/**
 * Get Tailwind text color class
 */
export function getTextColorClass(
  colorName?: string,
  fallback = "text-gray-900",
): string {
  if (!colorName) return fallback;
  return TEXT_COLOR_CLASSES[colorName] || fallback;
}

/**
 * Get Tailwind background color class
 */
export function getBgColorClass(
  colorName?: string,
  fallback = "bg-white",
): string {
  if (!colorName) return fallback;
  return BG_COLOR_CLASSES[colorName] || fallback;
}
```

Now when your CMS sends back a color name like `"primary"`, you pass it to `getBgColorClass()` and get back `'bg-blue-600'`—a static class name that Tailwind has already processed.

Use it in a component:

```typescript
// File: src/components/hero.tsx

import { getBgColorClass, getTextColorClass } from '@/lib/color-classes';

interface HeroProps {
  title: string;
  bgColor?: string;     // From CMS: "primary", "dark", etc.
  textColor?: string;   // From CMS: "white", "light", etc.
}

export function Hero({ title, bgColor, textColor }: HeroProps) {
  const bgClass = getBgColorClass(bgColor);
  const textClass = getTextColorClass(textColor);

  return (
    <section className={`${bgClass} ${textClass} py-20`}>
      <h1>{title}</h1>
    </section>
  );
}
```

When the CMS provides `bgColor: "primary"` and `textColor: "white"`, the component resolves these to `'bg-blue-600 text-white'` and applies them via `className`. Tailwind has already generated CSS for both classes.

## Real-World Application: Navbar Configuration System

Here's how this pattern solves the original problem at scale. If you're building a flexible navbar system where heights and colors are configurable per route or page:

```typescript
// File: src/lib/navbar-utils.ts

import { getNavbarPaddingClass } from "@/lib/navbar-classes";
import { getBgColorClass } from "@/lib/color-classes";

interface NavbarConfig {
  isFixed: boolean;
  height: number;
  bgColor?: string;
}

export function resolveNavbarConfig(cmsData: NavbarConfig) {
  const paddingClass = cmsData.isFixed
    ? getNavbarPaddingClass(cmsData.height)
    : "";
  const bgClass = getBgColorClass(cmsData.bgColor);

  return {
    paddingClass,
    bgClass,
    isFixed: cmsData.isFixed,
  };
}
```

Then in your page layout:

```typescript
// File: src/components/page-layout.tsx

import { resolveNavbarConfig } from '@/lib/navbar-utils';

interface PageLayoutProps {
  children: React.ReactNode;
  navbarConfig: NavbarConfig;
}

export function PageLayout({ children, navbarConfig }: PageLayoutProps) {
  const { paddingClass, bgClass } = resolveNavbarConfig(navbarConfig);

  return (
    <div className={paddingClass}>
      <nav className={bgClass}>
        {/* Navbar content */}
      </nav>
      {children}
    </div>
  );
}
```

The CMS can now control navbar height and background color completely, and your component applies static Tailwind classes that were generated at build time. No dynamic class generation, no parsing errors, no runtime styling surprises.

## Why This Is the Correct Pattern

Class registries aren't a workaround. They're the standard approach because they align with how Tailwind works. When you look at shadcn/ui components, you'll find this exact pattern throughout the codebase. They define static class mappings, use them consistently, and let Tailwind's build process handle everything.

The benefits:

**Type Safety**: Your registries are TypeScript objects. TypeScript catches typos in color names or missing height values.

**Consistency**: All styling goes through the same registry functions. You can't accidentally apply inconsistent classes.

**Maintainability**: Want to change a color or spacing value? Update one place in the registry. All components that use it automatically reflect the change.

**Performance**: No runtime style calculation. The class name lookup is a simple object property access.

**CMS Independence**: Your registries live in code, not in the CMS. Your CMS only provides semantic values (`"primary"`, `"dark"`, etc.), not raw CSS.

## Conclusion

Dynamic Tailwind classes seem like they should work, but they create a fundamental conflict with Tailwind's build-time CSS generation. Class registries solve this by moving the mapping from runtime to development time. You define all possible class combinations upfront in static objects, Tailwind generates CSS for all of them, and at runtime your component simply looks up the right class for the given CMS value.

This pattern scales beautifully. Whether you're mapping two values or twenty, adding new registries is straightforward. Start with one (like navbar heights), test it, then extend it to colors, spacing, typography, or any other CMS-driven style. You're building a design token system—the same approach that powers production design systems like shadcn/ui.

By the end of implementing this, you'll have a flexible, type-safe styling system that works seamlessly with your headless CMS, and you'll never hit a dynamic class generation error again.

Let me know in the comments if you run into edge cases or have questions about applying this to your specific setup, and subscribe for more practical development guides.

Thanks, Matija