We had a large project that needed to be split into multiple micro-frontends. We chose Module Federation to share modules across the application and Tailwind CSS for styling (v3). However, we encountered a significant challenge.
Our sample architecture consists of two federated remotes: Host and Common. The Host is the main React application with Tailwind CSS, while Common is a shared library, imported as a federated module used by both Host and Remote applications. Both use Tailwind CSS with a shared configuration.
Here’s the issue: Components in Host compile with the host’s styles.css
that includes all Tailwind imports, so Tailwind classes compile properly. However, components imported from Common don’t compile their Tailwind classes because they’re exported as separate units without access to the common styles.css
file. This means they lack the @tailwind
imports needed for class compilation.
Instead of manually importing @tailwind
styles in every component (which would be cumbersome), we can automate this with a custom loader in our webpack/rspack configuration.
css
/* tailwind.css */@tailwind base;@tailwind components;@tailwind utilities;
javascript
/* injectTailwind.js */module.exports = function (source) {if (source.startsWith('import')) {return `import "./tailwind.css";\n${source}`;}return source;};
javascript
/* rspack.config.js */// exposes is the map of modules exported in the module federation configconst filesToInjectTailwind = Object.values(exposes).map(file =>path.resolve(dirname, file),);module.exports = {// ... existing configmodule: {rules: [{test: /\.(ts|tsx|js|jsx)$/,include: filesToInjectTailwind,exclude: /node_modules/,use: [{loader: path.resolve(__dirname, './injectTailwind.js'),options: { name: moduleName },},],}]}};
This creates a new issue: CSS specificity conflicts. Since Host and Common compile Tailwind separately, they don’t know about each other, leading to specificity problems.
For example, if you use text-sm
in Host (applying font-size: 14px
) and text-lg
on a <Button />
component from Common (applying font-size: 18px
) on the same element, the Common styles will override the Host styles because Common’s styles.css
loads after Host’s.
Tailwind important
option: Scope all Tailwind classes to a parent class or ID
Tailwind prefixes: Add prefixes to each Tailwind class (e.g., host-text-sm
, common-text-lg
)
The Magic Bullet: A hybrid approach that’s much more practical
We combine the best of both approaches by using data attributes as CSS selectors.
We attach a data attribute to each element that uses Tailwind classes. All Host-rendered HTML tags get a host-remote
data attribute, and all Common-rendered HTML tags get a common-remote
data attribute.
html
<!-- host.module.html --><div host-remote class="text-sm">Host says Hello</div>
css
/* host styles.css */[host-remote].text-sm {font-size: 14px;}
html
<!-- common.button.html --><button common-remote class="text-lg">Click Me</button>
css
/* common styles.css */[common-remote].text-lg {font-size: 18px;}
javascript
/* injectDataAttribute.js */function injectDataAttributeJSX(source, attrValue) {return source.replace(/className=/g, `${attrValue}-mfe="" className=`);}module.exports = function (source) {if (source.startsWith('import')) {const { name } = this.getOptions();return injectDataAttributeJSX(source, name);}return source;};
javascript
/* rspack.config.js */module.exports = {module: {rules: [{test: /\.(ts|tsx|js|jsx)$/,exclude: /node_modules/,use: [{loader: path.resolve(__dirname, './injectDataAttribute.js'),options: { name: moduleName }, // moduleName is 'host' or 'common'},],}]}};
javascript
// postcss.config.jsmodule.exports = {plugins: {tailwindcss: tailwindConfig,autoprefixer: {},'postcss-prefix-selector': {prefix: `[${moduleName}-mfe]`,skipGlobalSelectors: true,transform(prefix, selector, _prefixedSelector, _filePath, rule) {if (rule.raws.tailwind && rule.raws.tailwind.layer !== 'base') {return `${prefix}${selector}`;}return selector;},},},};
Most projects use chunking for efficient loading, but this works against us here. If we have 20 components in the Common module that use the Tailwind class flex
, we’ll get 20 CSS chunks with the same flex
class. We need to combine them to avoid redundancy.
javascript
/* rspack.config.js */module.exports = {plugins: [new rspack.CssExtractRspackPlugin({filename: '[name].[hash].css',}),],...optimization: {splitChunks: {cacheGroups: {styles: {test: /\.css$/,name: `${moduleName}-styles`,chunks: 'all',enforce: true,},},},},};
With this setup, you now have full Tailwind CSS v3 support working properly with Module Federation. Each federated module maintains its own CSS scope while sharing components seamlessly across your micro-frontend architecture.