| |

Tailwind CSS 76 🎨 Server-Driven UI (SDUI) Utility Ingestion and Dynamic Runtime Styling

⏱️ Estimated read: 13 min | |

A server-driven UI reverses the usual direction of a front-end build. Instead of shipping the markup and the styles from the client bundle, the server sends a description of the interface, and the client renders it. The description may be a JSON tree, a set of components, or a list of the Tailwind class strings. The client interprets the description and produces the DOM. The model is the mobile-app pattern, the CMS-driven page builder, and the A/B testing platform. The problem is the same: the Tailwind engine scans the source at the build time, and the class from the server is not in the source. The client must have the CSS for the class it will render.

Key point: The Tailwind build-time engine does not exist at the runtime. The JIT scan is the build step, and the class that the server sends must be the class that the build produced. The runtime styling is about the mechanism that gets the server’s class into the CSS that the client already has, or the mechanism that generates the CSS at the runtime.


Why SDUI exists

The deploy problem. The new feature requires the new build, and the build requires the app-store review. The SDUI lets the server change the interface without the client deploy.

The personalization problem. The different users see the different layouts. The server chooses the layout, and the client renders it. The experiment and the segmentation are the server-side.

The A/B testing problem. The variant is the server’s decision. The client renders the variant from the description. The change is the immediate, and the measurement is the server’s.

The CMS problem. The content editor arranges the blocks and the layout. The block is the component, and the layout is the server’s data. The client renders the arrangement.

The build-time problem. The Tailwind engine scans the source. The class from the server is not in the source, and the class is not in the CSS. The runtime must have the mechanism.

The allowlist problem. The safe approach is the allowlist: the server may only use the classes the build included. The list is the contract, and the client renders only the known classes.

The runtime generation problem. The JIT at the runtime parses the class, looks up the theme, and injects the CSS. The approach is the flexible, and the cost is the bundle and the performance.


a. The allowlist

The allowlist is the set of the classes the build includes. The server may only use the classes in the set. The client renders the known classes.

The @source inline

The Tailwind v4 has the @source inline(...) that forces the class into the build.

@import "tailwindcss";

@source inline("bg-blue-600");
@source inline("bg-red-600");
@source inline("bg-green-600");
@source inline("text-white");
@source inline("px-4 py-2 rounded-md");

The classes are the safelisted, and the build produces the CSS.

The safe list from the config

The v4 supports the brace expansion in the @source inline.

@source inline("bg-{red,blue,green}-{500,600,700}");
@source inline("text-{sm,base,lg,xl}");

The brace expands to the many classes. The bg-red-500, the bg-red-600, the bg-red-700, and the same for the blue and the green.

The allowlist file

The allowlist is the JSON file the server and the client share.

// allowlist.json
{
  "colors": ["red-500", "red-600", "blue-500", "blue-600"],
  "spacing": ["1", "2", "4", "8"],
  "radius": ["sm", "md", "lg"]
}

The server composes the class from the allowed parts, and the client renders the class.

The server validation

The server validates the class against the allowlist before sending.

function validateClasses(classes) {
  return classes.every((cls) => allowlist.includes(cls));
}

const classes = ['bg-blue-600', 'px-4'];
if (!validateClasses(classes)) {
  throw new Error('The class is not in the allowlist');
}

The validation is the contract, and the server rejects the unknown.

The client rendering

function ServerComponent({ type, className, children }) {
  return <div className={className}>{children}</div>;
}

The className from the server is applied directly. The CSS is already in the bundle.

The allowlist table

The mechanismThe purpose
@source inline(...)The safelist
The brace expansionThe many classes
The allowlist JSONThe shared contract
The server validationThe reject the unknown
The client renderThe apply the class

b. The runtime generation

The runtime generation parses the class and injects the CSS. The approach is the flexible, and the cost is the bundle.

The twind

The twind is the runtime Tailwind. It parses the class and injects the CSS at the runtime.

import { twind, cssom, observe } from '@twind/core';
import presetTailwind from '@twind/preset-tailwind';

const tw = twind({
  presets: [presetTailwind()],
}, cssom(document));

observe(tw, document.body);

The twind parses the classes in the DOM and injects the matching CSS.

The class parsing

const css = tw('bg-blue-600 px-4 py-2');
// The CSS is the generated and injected.

The tw function returns the class name, and the CSS is the injected.

The performance cost

The runtime generation adds the JavaScript bundle (the twind is the roughly 15 KB) and the parsing at the runtime. The cost is the price of the flexibility.

The @twind/preset-tailwind

The preset is the Tailwind-compatible. The @twind/preset-tailwind is the v3-compatible, and the @twind/preset-tailwind-forms is the plugin.

The React integration

import { tw } from './twind';

function Component() {
  return <div className={tw('bg-blue-600 p-4')}>...</div>;
}

The tw returns the class, and the runtime injects the CSS.

The style injection

The cssom and the observe inject the CSS into the document. The twind watches the DOM and the generates the CSS for the new classes.

The runtime table

The toolThe purpose
twindThe runtime Tailwind
@twind/coreThe core
@twind/preset-tailwindThe preset
cssomThe style injection
observeThe DOM watch

c. The hybrid and the constraints

The hybrid combines the build-time and the runtime. The build includes the common, and the runtime generates the rare.

The build-time common

@import "tailwindcss";

@source inline("bg-{red,blue,green}-{500,600}");
@source inline("text-{sm,base,lg}");

The common classes are the safelisted, and the build produces the CSS.

The runtime rare

const runtimeClasses = serverClasses.filter((cls) => !buildClasses.includes(cls));
const runtimeCSS = tw(runtimeClasses.join(' '));

The rare classes are the generated at the runtime. The common is the build.

The constraints

The runtime generation has the constraints: the oklch() and the modern functions are not supported in the old browsers, the custom variant may not work, and the arbitrary values are the parsed at the runtime.

The CSP problem

The runtime style injection may violate the Content Security Policy. The style-src 'unsafe-inline' is required, and the CSP is the weaker.

The SSR problem

The server renders the HTML, and the client hydrates. The runtime generation on the server and the client must match, and the mismatch causes the hydration error.

The cache problem

The runtime classes are not in the build cache. The repeated class is the parsed each time, and the twind caches the result.

The hybrid table

The aspectThe build-timeThe runtime
The commonThe build—
The rare—The runtime
The bundleThe CSSThe JS
The CSPThe strictThe unsafe-inline
The SSRThe stableThe mismatch risk

Complete Example Session

/* ============================================
   PART 1: THE ALLOWLIST
   ============================================ */

@import "tailwindcss";

@source inline("bg-blue-600");
@source inline("bg-red-600");
@source inline("bg-green-600");
@source inline("text-white");
@source inline("px-4 py-2 rounded-md");

/* ============================================
   PART 2: THE BRACE EXPANSION
   ============================================ */

@source inline("bg-{red,blue,green}-{500,600,700}");
@source inline("text-{sm,base,lg,xl}");

/* ============================================
   PART 3: THE ALLOWLIST JSON
   ============================================ */

{
  "colors": ["red-500", "red-600", "blue-500", "blue-600"],
  "spacing": ["1", "2", "4", "8"],
  "radius": ["sm", "md", "lg"]
}

/* ============================================
   PART 4: THE SERVER VALIDATION
   ============================================ */

function validateClasses(classes) {
  return classes.every((cls) => allowlist.includes(cls));
}

const classes = ['bg-blue-600', 'px-4'];
if (!validateClasses(classes)) {
  throw new Error('The class is not in the allowlist');
}

/* ============================================
   PART 5: THE CLIENT RENDER
   ============================================ */

function ServerComponent({ type, className, children }) {
  return <div className={className}>{children}</div>;
}

/* ============================================
   PART 6: THE TWIND SETUP
   ============================================ */

import { twind, cssom, observe } from '@twind/core';
import presetTailwind from '@twind/preset-tailwind';

const tw = twind({
  presets: [presetTailwind()],
}, cssom(document));

observe(tw, document.body);

/* ============================================
   PART 7: THE RUNTIME CLASS
   ============================================ */

const css = tw('bg-blue-600 px-4 py-2');
// The CSS is generated and injected.

/* ============================================
   PART 8: THE REACT INTEGRATION
   ============================================ */

import { tw } from './twind';

function Component() {
  return <div className={tw('bg-blue-600 p-4')}>...</div>;
}

/* ============================================
   PART 9: THE HYBRID
   ============================================ */

const runtimeClasses = serverClasses.filter((cls) => !buildClasses.includes(cls));
const runtimeCSS = tw(runtimeClasses.join(' '));

/* ============================================
   PART 10: THE COMPLETE PATTERN
   ============================================ */

// 1. The build scans the source, not the server.
// 2. The server's class is not in the source.
// 3. The allowlist is the contract.
// 4. The @source inline forces the class.
// 5. The runtime generation is the flexible.
// 6. The twind is the runtime Tailwind.
// 7. The CSP is the constraint.
// 8. The SSR must match.
// 9. The hybrid combines the both.
// 10. The validation is the server.

The ten parts showed the allowlist, the brace expansion, the allowlist JSON, the server validation, the client render, the twind setup, the runtime class, the React integration, the hybrid, and the complete pattern.


Quick Reference

The Allowlist

The mechanismThe purpose
@source inline(...)The safelist
The brace expansionThe many classes
The allowlist JSONThe contract
The server validationThe reject
The client renderThe apply

The Runtime Tools

The toolThe purpose
twindThe runtime Tailwind
@twind/coreThe core
@twind/preset-tailwindThe preset
cssomThe injection
observeThe DOM watch

The Constraints

The aspectThe buildThe runtime
The bundleThe CSSThe JS
The CSPThe strictThe unsafe-inline
The SSRThe stableThe mismatch
The cacheThe buildThe runtime
The flexibilityThe lowThe high

The Patterns

The patternThe mechanism
The pure allowlistThe @source inline
The pure runtimeThe twind
The hybridThe both
The validationThe server

The CSP Considerations

The directiveThe value
style-srcThe 'unsafe-inline'
The nonceThe alternative
The hashThe alternative
The strictThe runtime disabled

Best Practices

✅ Do This:

/* Use the @source inline for the allowlist. */
@source inline("bg-{red,blue,green}-{500,600}");  /* ✅ */
// Validate the server's class.
if (!validateClasses(classes)) throw new Error('The unknown class');  // ✅
// Use the twind for the runtime generation.
const tw = twind({ presets: [presetTailwind()] }, cssom(document));  // ✅
// Use the hybrid for the common and the rare.
const runtimeClasses = serverClasses.filter((cls) => !buildClasses.includes(cls));  // ✅
// Apply the server's class directly.
<div className={className}>{children}</div>;  // ✅
// Use the CSP nonce for the runtime styles.
const nonce = getNonce();  // ✅

❌ Don’t Do This:

// Don't expect the build to include the server's class.
<div className={serverClass}>;  // ❌ The CSS is not generated
// Don't skip the validation.
sendClass(serverClass);  // ❌ The unknown class
// Don't use the runtime generation without the reason.
const tw = twind(...);  // ⚠️ The bundle and the CSP
// Don't rely on the CSP unsafe-inline.
style-src 'unsafe-inline';  // ❌ The weaker
// Don't mismatch the SSR and the client.
// The hydration error.  // ❌
// Don't generate the class at the runtime for the common.
tw('bg-blue-600');  // ⚠️ The build should include it

Common Pitfalls

The pitfallThe reasonThe fix
The class has no CSSNot in the sourceUse the @source inline
The class rejectedNot in the allowlistAdd to the allowlist
The CSP blocksThe runtime injectionUse the nonce
The hydration mismatchThe SSR differsMatch the both
The bundle growsThe runtimeUse the hybrid
The cache missThe runtimeUse the build
The validation passesNo checkAdd the server validation

Real-World Examples

1. The @source inline

@source inline("bg-blue-600");

2. The Brace Expansion

@source inline("bg-{red,blue,green}-{500,600}");

3. The Allowlist JSON

{ "colors": ["red-500", "blue-500"] }

4. The Server Validation

if (!validateClasses(classes)) throw new Error('Unknown');

5. The Client Render

<div className={className}>{children}</div>

6. The Twind Setup

const tw = twind({ presets: [presetTailwind()] }, cssom(document));

7. The Runtime Class

const css = tw('bg-blue-600 px-4 py-2');

8. The React Integration

<div className={tw('bg-blue-600 p-4')}>

9. The Hybrid

const runtimeClasses = serverClasses.filter((cls) => !buildClasses.includes(cls));

10. The Complete Pattern

@source inline("bg-{red,blue,green}-{500,600}");

Visual

The SDUI Flow

┌────────────────────────────────────────────────────────────┐
│  THE SDUI FLOW                                             │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE SERVER                                        │     │
│  │    The JSON: { "className": "bg-blue-600 p-4" }    │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           │  The response                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE CLIENT                                        │     │
│  │    The render: <div className="bg-blue-600 p-4">   │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           │  The CSS lookup              │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE CSS                                           │     │
│  │    The build produced the .bg-blue-600.            │     │
│  │    The build produced the .p-4.                    │     │
│  │    The allowlist guarantees the class.             │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Allowlist

┌────────────────────────────────────────────────────────────┐
│  THE ALLOWLIST                                             │
│                                                            │
│  THE BUILD                                                 │
│  ┌──────────────────────────────────────────────────┐     │
│  │  @source inline("bg-{red,blue,green}-{500,600}");  │     │
│  │    → the .bg-red-500, the .bg-red-600, ...          │     │
│  │    → the CSS is in the bundle.                     │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE SERVER                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The class: "bg-blue-600 p-4"                      │     │
│  │    → the validation: in the allowlist?             │     │
│  │    → the response: { className: "..." }            │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE CLIENT                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <div className="bg-blue-600 p-4">                 │     │
│  │    → the CSS is already in the bundle.             │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Runtime Generation

┌────────────────────────────────────────────────────────────┐
│  THE RUNTIME GENERATION                                    │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  const tw = twind({ presets: [presetTailwind()] });│     │
│  │                                                   │     │
│  │  <div className={tw('bg-blue-600 p-4')}>           │     │
│  │    → the tw parses the class                       │     │
│  │    → the CSS is generated                          │     │
│  │    → the CSS is injected                           │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE COST                                                  │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The bundle: +15 KB (the twind)                    │     │
│  │  The parse: at the runtime                         │     │
│  │  The CSP: the 'unsafe-inline'                      │     │
│  │  The SSR: the mismatch risk                        │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Hybrid

┌────────────────────────────────────────────────────────────┐
│  THE HYBRID                                                │
│                                                            │
│  THE BUILD                                                 │
│  ┌──────────────────────────────────────────────────┐     │
│  │  @source inline("bg-{red,blue,green}-{500,600}");  │     │
│  │    → the common classes                            │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE RUNTIME                                               │
│  ┌──────────────────────────────────────────────────┐     │
│  │  const runtimeClasses = serverClasses.filter(      │     │
│  │    (cls) => !buildClasses.includes(cls)            │     │
│  │  );                                                │     │
│  │  const runtimeCSS = tw(runtimeClasses.join(' '));  │     │
│  │    → the rare classes                              │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The common is the build, the rare is the runtime.         │
└────────────────────────────────────────────────────────────┘

Summary

The itemThe value
The SDUIThe server-driven UI
The allowlistThe safelisted classes
@source inlineThe safelist mechanism
The brace expansionThe many classes
The server validationThe reject the unknown
The twindThe runtime Tailwind
The hybridThe both
The CSPThe constraint
The SSRThe mismatch risk
The cacheThe build vs the runtime

Key takeaways:

  • The Tailwind build-time engine does not exist at the runtime. The JIT scan is the build step, and the class the server sends must be the class the build produced. The allowlist is the contract.
  • The @source inline(...) forces the class into the build. The safelisted class is in the CSS, and the server may use it. The brace expansion produces the many classes from the one entry.
  • The server validates the class against the allowlist. The unknown class is rejected, and the client never renders the unstyled element. The validation is the contract.
  • The twind is the runtime Tailwind. It parses the class and injects the CSS at the runtime. The cost is the bundle, the parse, and the CSP.
  • The CSP is the constraint. The runtime style injection requires the style-src 'unsafe-inline' or the nonce. The strict CSP is the build-time only.
  • The SSR must match the client. The runtime generation on the server and the client must produce the same CSS, or the hydration fails.
  • The hybrid combines the both. The common classes are the build, and the rare classes are the runtime. The pattern reduces the bundle and the CSP impact.
  • The allowlist is the primary. The safe approach is the safelist with the validation. The runtime generation is the fallback for the truly dynamic.

Remember: The server-driven UI and the dynamic runtime styling are the pattern where the server decides the interface. The Tailwind build-time engine does not see the server’s class, and the allowlist, the @source inline, and the runtime generation are the mechanisms. The result is the interface the server can change without the deploy, and the CSS the client already has.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!