CSS isolation with Angular Micro frontends

Eugene Pavliy
8 min readJul 25, 2022

--

Recently I’ve been working with project where micro frontend architecture was required. And found interesting challenges to share with you, folks.
In this article we will be working in context Angular, but it’s possible also to use other frameworks, of course taking some specifics into account.

Micro frontends are great

Micro frontends topic is very popular nowadays. We all want to build independent teams and scale our development process well.

There are many techniques how to build microfrontend apps. One of them, which I like a lot — using Webpack Module federation. If you’d like to get more details on it — highly recommend THIS great series from angular architects (Manfred Steyer)

Why isolation & CSS isolation

Micro frontends taste best when teams are independent and work in own sandboxes, in isolation. To make it happen — series of steps should be implemented.

Angular + Webpack federation + WebComponent + ShadomDom = recipe I really like

  • Webpack federation allows to easily load microfrontend code. Well, it can be any JS code, of course. But we’re talking about microfrontends here.
  • WebComponents allow to define custom elements which will bring sort of encapsulation in a game. In Angular — the best way to do it is using Angular Elements
  • ShadowDom — allows to encapsulate CSS. Imagine that you can have your own CSS world inside of already load HTML page. And root/parent CSS cannot break your CSS anymore…. In addition — your isolated CSS — won’t break root/parent as well. Isn’t that nice?

Isolation allows us to develop in safe manner. You don’t need to care if someone breaks your styles or if you break someones styles.

What’s the main concern of it? Well, it’s duplication. To make things work we will need to duplicate some stuff at different levels. But whether it’s a problem or not — really depends on your scenario. For example, if there is a plan to have 1–2 micro apps at the same page — it’s fine. But if you desire to have lots of micro apps on the same page and it’s actively used by mobile users — duplication might bring issues. For CSS, BTW, some of such issues can be fixed — we will take a look how.

Let’s practice

First of all let’s describe our conditions.

  1. There is a Shell. Developed by teamA. It’s considered as an entry point for the customer and accessible let’s say via https://resource-portal.test.com
    Shell loads MFEs using module federation and displays them as web components.
  2. There is MFE application. Developed by teamB. It shares application as WebComponent and lets Shell to host it. At the same time — it’s also accessible as standalone app via https://mfe-app.test.com

Assuming we need to have responsive design, we decide to use REM as measure unit. BTW, if you’re not with REM yet — it’s highly on demand now, especially if you develop responsive app. You can read more about REM usage here.

Main goal is to ensure that Shell & MFE — are not conflicting in terms of CSS = operate in isolation. So that teams can feel safe.

Overall, it’s basic microfrontend setup with shell and one microfrontend application.

So we will be touching multiple practical cases which I think will be useful for your development process. Most of described changes will be happening on MFE side.

Enable ShadowDom for MFE

In Angular it’s really easy to enable ShadowDom. Set ViewEncapsulation.ShadowDom — in our case that will be for app component inside MFE which we share using module federation.

encapsulation: ViewEncapsulation.ShadowDom

Inject styles inside WebComponent

Once appropriate encapsulation is in place — Angular global/parent styles are not in action anymore. Our component lives on its own. So we have to inject required CSS directly into component .scss.

If we use chrome dev tools to see source — we will noticed that there are <style> tags inside MFE web component, which is good.

I usually transfer all my css from styles.sccs into app.component.scss
Also it’s needed to inject all 3rd parties like corporate themes, Material styles etc.

The side effect of such injection — bundle size. Cause we duplicate CSS for each instance of web component including all 3rd parties we need. But we win a lot with isolation benefits, right?

No worries, there is one way to make things better — using CDN. And linking CSS from there instead of direct/local injection. Thanks to browsers — they usually cache CSS from CDN. That means — first instance will load needed CSS and next one will just grab that from cache. Also if your app is not huge enough and has small amount of 3rd parties — there shouldn’t be a worry about bundle size.

Inheritable styles leaking — stop it

Out of the box even with shadow DOM — some of styles are leaking from top level (in our case it’s from Shell). Because they are inheritable. For instance: color, cursor, font etc.

There is a great article related — highly recommended to read.
To fix this — simply add pointed css to your app.component.

:host {
all: initial;
}

Be careful with :host

We tuned all styles, checked in standalone mode and all works well. But inside Shell — some of styles are not applied. For instance, global css variables.

To me that usually happened while using :host in css. During investigation — found that it’s needed to add :root as well. Not sure why it happens, but this is just an observation — while working in standalone — :host is used, but when inside Shell — :root is used.

:root div, :host div => works.

Say good bye to SharedStyleHost styles

So we’ve injected all needed CSS into app component css directly. And now use that web component in same project (putting it into index.html).

Expectation is to have that only in web component shadow DOM. However, if you look at document head — you’ll see much more styles there. Curious, why?

Turned out there is a bug at the moment and styles are coming to the top as well: https://github.com/angular/angular/issues/35039

To fix this — we need to cleanup shared styles host:

NOTE: if you share web component with module federation and create this web component in another app — this issue will not be in place

Set some baselines

Now when we look at MFE inside Shell — all looks good. However, when working as standalone — some issues might appear. Remember, at start we’ve mentioned REM usage?

So to make REM work fine in standalone — we need to set baseline at top level, which is usually html tag. And in our case — it’s usually set on Shell side. That means — when using MFE inside Shell — all works fine. But in standalone — it’s not. And you cannot push that baseline into web component itself. Solution is to set baseline on MFE site as well.

html {
// To set baseline for rem
font-size: 62.5%;
}

Angular Material. Is it in a game?

Ok, teamB decides to use Material lib. But Shell owners (teamA) don’t use it. And you probably know that Material likes to use global styles and scope a lot.

We could ask Shell team to inject Material for us. But that breaks independency. And it will not work as well, because styles won’t leak into MFE web component. So it doesn’t fit

To make things work — we have to inject Material into MFE app.component.scss

This will allow to inject Material styles into MFE component itself. So it’s independent and self-contained now. Remember about the bundle size, though and fact that there is a way to improve it using CDN instead of such direct injection.

Oh no… Material Overlays?

We’re happy, Material works fine. But once we start using components where overlay is used — things work really bad.

By default — overlays are added to document root. It’s logical, because libs can position it correctly and cleanup things when done.
However, what happens in our case? MFE Web component uses ShadowDom and ignores all styles from Shell. Overlay itself is added to document root — it’s area of Shell. Do we have Material styles there? Right, we don’t. And that’s the issue.

To solve this — we need to add overlay container inside WebComponent — meaning inside its shadow root.

The good thing is that Material allows to handle that easily.

Keep in mind — not all libs are friendly to shadow root at the moment. But you can often contribute and help to make it happen faster.

And then, we should go to app.module.ts and add:

{
provide: OverlayContainer,
useClass: WebComponentOverlayContainer,
},

This will result in fact that overlay container will get into web component shadow root. And all injected styles we have there will be affecting inserted overlay. Win!

Wait… @font-face not working?

Again in context of Material, but not limited to it. Imagine that teamB wants to use custom fonts in their MFE application. @font-face is well-known stuff to work with custom fonts.

So they go to app.component.html and put link to font on top. And then try to use it in scss
But… It doesn’t work and Material icons are not visible…

That’s expected due to specification of how@font-face works now. To resolve — it’s needed to load fonts explicitly from the top of web page. And then use it in needed place.

The good thing — we don’t need to ask Shell team to do anything on that. We can simply inject that script ourselves. Note please, that this of course affects our independency, cause we push to top Shell scope where other MFEs will be able to push that fonts as well. But there is no other choice at the moment.

Sometimes it makes sense to have agreement between MFE and Shell owners and share such items once and globally.

The code to load fonts from MFE can look like below:

Query Dom — remember about ShadowRoot

In some case you might need to query element by selector. In a situation — when there is shell and MFE web component — try to query element by ID inside MFE using document.querySelector— no luck, right? Why?
Well… Once you start working with webcomponents and shadow dom — you have to specify shadow root context to search in.

So instead of document.querySelector use document.querySelector('<web_component_name'>).shadowRoot.querySelector('') and you’re good.

Wrapping up

At this stage I guess we’re done. As you can see working in CSS isolation requires some specifics knowledge for each team which is involved. But once all those specifics are transparent and understood — it’s much easier to develop things and do not worry about breaking things.

Is it a must to use CSS isolation? For sure not. As usual, watch the needs of your product and your teams.

--

--