DEV Community

Cover image for Tailwind in Micro‑Frontends
Asmaa Almadhoun
Asmaa Almadhoun

Posted on

Tailwind in Micro‑Frontends

From Token Drift to a Single Source of Truth

1) Context & Problem

When you scale a Module Federation architecture (shell + multiple independently deployed micro apps), it’s tempting to let every micro app own its Tailwind config. That seems harmless until it isn’t.
Signs you likely saw:

  • Token drift: primary, secondary, and other design tokens differ per micro app.
  • Inconsistent UI: The shell and remotes looked related but not identical.
  • Copy‑paste debt: Colors, shadows, and breakpoints duplicated across repos.
  • Refactor pain: Changing a brand variable meant N pull requests.
  • Conflicting semantics: primary in one app wasn’t the same hex in another.

Root causes:

  • Each repo has its own “design system.”
  • No enforced tokens contract shared at build time and runtime.
  • Different teams tweaked Tailwind locally, with no common presence.
  • Pre-flight, prefixes, and naming were sometimes inconsistent.

Result:

  • Slower brand rollouts, visual inconsistencies, and higher QA costs.

2) Design Goals
To reverse the drift without killing micro‑app independence:

  • Single Source of Truth (SSOT) for tokens, colors, typography, spacing, shadows, breakpoints.
  • One Tailwind prepared for every micro app import, no duplication.
  • Runtime theming via CSS variables (dark mode, brand variants) controlled by the shell.
  • Module Federation‑friendly: shared version of the tokens package, and predictable CSS load order.
  • Minimal migration friction: Keep class names like text-primary, bg-colors, etc.

3) Target Architecture (High‑Level)

@org/tokens (npm package)

  • tokens.css → CSS variables for runtime theming.
  • tailwind.preset.cjs → Tailwind preset mapping semantic keys (colors, shadows, screens).
  • Optional: boxShadow, spacing, fontFamily, etc.

Shell (host)

  • Loads tokens.css once and sets theme attributes (e.g., data-theme="dark").
  • Shares @org/tokens in Module Federation to keep a single version.
  • Enables Tailwind preflight once (recommended) and optionally a class prefix for safety.

Remote micro apps

  • Import the Tailwind preset from @org/tokens.
  • Rely on shell‑injected tokens.css (or import defensively if shell can’t).
  • Use semantic classes (e.g., text-primary, bg-bg, shadow-card) that are all defined from tokens.

4) The Tokens Package

4.1 Minimal tokens.css (runtime variables & themes)

/* packages/tokens/tokens.css */color-bg: #FFF5F0;
}

/* Optional dark theme (shell can toggle) */
:root[data-theme="dark"] {
  --color-text: #F3F4F6;
  --color-bg: #0B1220;
}
``
:root {
  --color-primary: #4A90E2;
  --color-text: #1F2A37;
Enter fullscreen mode Exit fullscreen mode

4.2 Minimal Tailwind preset (map variables → Tailwind theme)

// packages/tokens/tailwind.preset.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: 'var(--color-primary)',
        text: 'var(--color-text)',
        bg: 'var(--color-bg)',
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

5) Remote Micro App, Smallest Possible Integration

5.1 Federation config (Remote)

// apps/appA/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  // ...
  plugins: [
    new ModuleFederationPlugin({
      name: 'appA',
      filename: 'remoteEntry.js',
      exposes: {
        './Widget': './src/Widget', // whatever you expose
      },
      shared: {
        react: { singleton: true, requiredVersion: deps.react },
        'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },

        // 🔑 Same tokens instance/version as shell
        '@org/tokens': { singleton: true, eager: true },
      },
    }),
  ],
};

Enter fullscreen mode Exit fullscreen mode

5.2 Tailwind config (Remote)

// apps/appA/tailwind.config.cjs
const { preset } = require('@org/tokens');

module.exports = {
  presets: [preset],                    // 🔑 central config
  content: ['./src/**/*.{ts,tsx,html}'],
  prefix: 'tw-',                        // match shell if you prefix
  corePlugins: { preflight: false },    // let shell handle the reset
};
``
export default function Card() {
  return (
    <div className="tw-bg-bg tw-text-text tw-rounded-xl tw-p-4 tw-border tw-border-primary">

Enter fullscreen mode Exit fullscreen mode

If you’re not 100% sure the shell always loads tokens.css, you can optionally import it in the remote entry as a fallback:

import '@org/tokens/tokens.css';
Enter fullscreen mode Exit fullscreen mode

5.3 Stylesheet entry

  • Preferred: Let the shell load tokens.css globally.
  • Defensive (if shell might not): import it here too (duplicates are harmless).
/* apps/appA/src/index.css */
@import '@org/tokens/tokens.css';

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

5.4 Component usage (Remote)

// apps/appA/src/components/Card.tsx
export function Card({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <div className="tw-bg-cardbg tw-text-text tw-rounded-2xl tw-shadow-card tw-transition tw-border-t-4 tw-border-primary tw-p-6">
      <h3 className="tw-text-primary tw-font-bold tw-mb-2">{title}</h3>
      <div className="tw-text-muted">{children}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The classes resolve to your tokens because the preset maps Tailwind theme keys to CSS variables from tokens.css. When the shell toggles data-theme or data-brand, every remote instantly follows.

7) Operational Tips

Versioning & Changelog: Treat @org/tokens like a product.

  • MAJOR for breaking renames/removals.
  • MINOR for added tokens.
  • PATCH for non‑breaking fixes.

Guardrails:

  • Lint to forbid raw hex in apps, use tokens only.
  • Consider codemods to migrate existing classes to semantic ones.
  • Visual regression tests in shell for critical flows (Chromatic/Playwright/etc).

Avoid CSS Leaks:

  • Use Tailwind prefix to reduce naming collisions.
  • Keep preflight in shell only.
  • If any remote uses Shadow DOM, it won’t inherit CSS variables from the document; re‑inject tokens.css inside that shadow root.

Theming Strategy:

  • Shell sets data-theme and data-brand at or at each micro app’s mount container if you want per‑app themes.
  • Tailwind utilities simply reference the semantic theme keys provided by your preset (no rebuild required at runtime).

8) Migration Plan (Practical)

  • Publish @org/tokens with tokens.css + tailwind.preset.cjs.
  • Wire shell to load tokens.css, set theme attributes, and share the package in MF.
  • Update each remote to use presets: [preset] and disable preflight.
  • Replace raw hex and local color names with text-text, bg-bg, text-primary, border-accent, etc.
  • Cut a brand update (change a token value) and watch all apps update without per‑repo edits.

Top comments (0)