Learn Tailwind CSS - Handling Dynamic Content & Security Considerations
Episode 11 of 23

Learn Tailwind CSS - Handling Dynamic Content & Security Considerations

This episode covers the risks of dynamic classes from user input, safe validation and safelist patterns, and how to avoid injection via class names. You also learn content scanning strategies for multi-tenant environments with proper isolation.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Dynamic content is a fact of life in real applications: colors from a database, sizes from user preferences, themes from configuration. But how you combine those values into a class attribute carries two risks: classes not generated by JIT, and more seriously — CSS injection through class names. Episode 11 covers both.

The first risk leaves UI unstyled; the second can be a security hole. Tailwind supports arbitrary values like p-[calc(100%-1rem)], and if that value comes from a user without validation, the user can inject dangerous arbitrary values into your stylesheet.

Risks of Dynamic Classes from User Input

The most problematic pattern is assembling class names from string fragments:

JSA dangerous pattern
const size = userInput; // nilai dari database atau form
const className = "p-" + size; // berbahaya

Two problems arise: JIT doesn't see the complete string p-4 in content, so the class isn't generated; and if size contains an arbitrary value, a user can inject arbitrary CSS rules through the arbitrary values feature. Avoid this pattern entirely.

Validation and Safelist Patterns

Replace assembly with explicit mapping — every allowed value maps to a fully written class string:

JSMapping values to full classes
const sizeMap = {
  small: "p-2 text-sm",
  medium: "p-4 text-base",
  large: "p-6 text-lg",
};
 
const className = sizeMap[size] ?? sizeMap.medium;

Class strings like "p-4 text-base" appear whole in content, so JIT generates them. Unknown values fall back to the sizeMap.medium default. This sizeMap[size] ?? sizeMap.medium pattern is safe, deterministic, and easy to test.

If mapping isn't feasible because the number of combinations is large but limited, use safelist:

JSSafelist for a limited set
safelist: [
  "bg-red-500",
  "bg-green-500",
  "bg-blue-500",
  "text-red-500",
  "text-green-500",
  "text-blue-500",
],

Safelist forces those classes to always be generated. Keep its use limited — every entry adds CSS size regardless of use.

Avoiding Injection via Class Names

CSS injection happens when user values enter the class attribute and Tailwind accepts them as arbitrary values. Two layers of defense:

  • Value allowlist: only accept values that exist in the mapping, reject the rest.
  • Validate before rendering: make sure the value doesn't contain rule-shaping characters like [, :, and ;.

A simple validation example:

JSValidating input before use
const allowedSizes = new Set(["small", "medium", "large"]);
const safeSize = allowedSizes.has(size) ? size : "medium";

allowedSizes.has(size) ? size : "medium" guarantees only three values ever reach the markup. There's no path for a user-supplied arbitrary value to enter the class attribute.

Warning

Never put user data directly into a class attribute. If your application really must accept class names from outside, validate against an allowlist first, then render as a complete string — not as assembled fragments.

Content Scanning for Multi-tenant

In a multi-tenant environment, a single Tailwind instance serves many tenants with different themes. The challenge: if all tenants are scanned together, all tenants' classes end up in one stylesheet — wasteful and risky for token leakage between tenants.

Recommended strategies:

  • Build per tenant: run Tailwind with per-tenant content and config, generating a separate stylesheet per tenant.
  • Use a single design token surface: restrict the classes tenants can use to an already-safelisted set, so the stylesheet is uniform and safe.
  • Avoid arbitrary values from tenant input: arbitrary values must always come from server configuration, never from client requests.

An example of per-tenant builds on the CLI:

Build a stylesheet per tenant
npx tailwindcss -i ./themes/tenant-a.css -o ./dist/tenant-a.css
npx tailwindcss -i ./themes/tenant-b.css -o ./dist/tenant-b.css

Each tenant gets a separate CSS file containing only the classes allowed for it — the result of truly isolated scanning.

In multi-tenant environments with SSR or API-driven flows, make sure the content list only includes files that actually write class names — templates and components, not runtime data. Scanning database contents adds build load with no benefit, because JIT only recognizes string literals in source, not values injected from outside.

Conclusion

Episode 11 closed the security and reliability gaps in dynamic content: explicit mapping replaces class name assembly, safelist handles limited sets, allowlist validation blocks injection, and per-tenant builds provide isolation in multi-tenant setups.

Key takeaways:

  • Never assemble class names from string fragments.
  • Explicit mapping lets JIT see complete strings and limits values.
  • Safelist is only for limited, known class sets.
  • CSS injection is prevented with allowlist validation before rendering.
  • Multi-tenant should build per tenant with restricted tokens.
  • Arbitrary values must come from server configuration, not the client.

Next, in episode 12, we'll cover delivery & CDN — comparing build-time generation with the Play CDN, CSP and SRI security when using a CDN, and caching strategies and HTTP headers for static CSS files.

Learn Tailwind CSS - Handling Dynamic Content & Security Considerations | Learn Tailwind CSS