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;
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)',
},
},
},
};
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 },
},
}),
],
};
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">
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';
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;
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>
);
}
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)