Photo by Maik Jonietz on Unsplash

Managing Scoped Styles in Module Federation

Alp Gökçek

--

In the world of modern web development, micro frontends are becoming increasingly popular. One key technology making micro frontends possible is Webpack’s Module Federation. This feature allows different Webpack builds to work together and share code at runtime. However, managing CSS in Module Federation applications can be challenging.

In this article, I will explain how we handled these CSS challenges. We will share the strategies we used, including a custom PostCSS plugin we developed. By sharing our approach, we hope to help other developers facing similar issues in their micro frontend projects.

The CSS Challenge

When it comes to CSS, traditional methods of including styles may lead to conflicts and inconsistencies. This is especially true in a Module Federation setup where multiple teams might use different CSS methodologies, preprocessors, or frameworks. The key challenges include:

  • CSS Scope Isolation: Avoiding style clashes between different micro frontends.
  • CSS Loading Order: Ensuring styles are loaded in the correct order.
  • Shared Styles: Managing common styles shared across different micro frontends.

In our project, we were using the Tailwind CSS framework. When integrating Tailwind into our micro frontend applications, we encountered conflicts that resulted in unwanted views.

Initial Solution: Adding Prefixes

The most straightforward solution was to add a prefix to each classname. In Tailwind’s configuration, there is an option to add a prefix:

module.exports = {
prefix: 'app1-',
darkMode: ['class'],
content: ['./app/**/*.{js,ts,tsx,html}', './src/**/*.{js,ts,tsx,html}']
}

However, configuring it this way required us to change all classnames to start with the prefix:

<!-- before -->
<div className="flex gap-3 mb-2 w-full"/>

<!-- after -->
<div className="app1-flex app1-gap-3 app1-mb-2 app1-w-full"/>

While this approach worked, it made the code harder to write and maintain. For example, we forgot to add these prefixes in such cases and tried to find the issue. We needed a different solution.

Our Solution: Custom PostCSS Plugin

To solve this issue more elegantly, we developed a custom PostCSS plugin that wraps all CSS selectors with a specific parent selector. This ensured that styles were scoped to a particular micro frontend, preventing conflicts with other parts of the application.

Custom PostCSS Plugin Example

Here is the custom PostCSS plugin we created:

// postcss.config.js
const tailwindcss = require('tailwindcss');
const wrapSelector = (opts = {}) => ({
postcssPlugin: 'wrap-selector',
Once(root) {
root.walkRules((rule) => {
if (!rule.selectors) return rule;
rule.selectors = rule.selectors.map((selector) => `${opts.wrapper} ${selector}`);
});
},
});
wrapSelector.postcss = true;

module.exports = {
plugins: ['postcss-preset-env', tailwindcss, wrapSelector({ wrapper: '#app1-id' })],
};

How It Works

  1. Plugin Definition: The wrapSelector function is defined as a PostCSS plugin. It takes an opts object that includes the wrapper property, which is the parent selector to wrap around existing CSS selectors.
  2. Walking Through Rules: The Once method walks through all CSS rules. For each rule with selectors, it maps each selector to a new one prefixed with the specified wrapper.
  3. PostCSS Configuration: The plugin is then included in the PostCSS configuration along with other plugins like postcss-preset-env and tailwindcss.

Usage in Module Federation

To use this custom PostCSS plugin in a Module Federation setup, follow these steps:

  1. Configure PostCSS: Ensure your PostCSS configuration includes the custom plugin as shown above.
  2. Apply Scoped Styles: Wrap the CSS selectors in the micro frontend application to ensure styles are scoped appropriately.

Example Configuration in Webpack

Here’s how you can integrate the PostCSS plugin into your Webpack configuration:

// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader', 'postcss-loader'],
}
],
},
};

Example Usage in a React Component

import React from 'react';

const App = () => {
return (
<div id="app1-id">
<h1 className="text-2xl font-bold">Hello from App 1</h1>
</div>
);
};

export default App;

Conclusion

Handling CSS in Module Federation applications requires careful consideration of scope isolation, loading order, and shared styles. By leveraging custom PostCSS plugins, such as the one demonstrated above, you can effectively manage CSS and ensure styles are properly scoped to individual micro frontends. This approach helps maintain a modular and conflict-free CSS architecture in your micro frontend applications.

--

--