Avoiding CSS style collisions when building a UI widget
Tags: css
Published: 2022-07-01 01:06:15

Use case: you're tasked with developing a UI widget, that can be used in any integrator webpage. 

Classical examples: Google Maps, chat widget. 

Problem: some integrators have conflicting requirements:
- Make sure there is no style collision between the integrator-specific style and your widget style
- Allow the widget to be customised with integrator-specific custom style

# The bad approach

First, let's start with the simple, but very risky approach. You can have the widget and the integrator style side-by-side, without anything in place to create any isolation, and _hope_ it will work. However, in reality, there can be a wide variety of CSS style conflicts. 

Examples:
- CSS selectors of the integrator can have higher selectivity than the ones used in the widget (example: `#root .button` > `.button`)
- common CSS selectors (such as `.root`, `.button`) might be used by both the integrator and the widget code, without being aware of each-other
- global style can pollute elements in either direction (example: `*` selector used in the widget or integrator styles)
- wide-impact _element_ selectors (example: `div`, `img`)
- `!important` (used by the integrator own source code, or part of some third party library outside their control)

To accommodate the 2 requirements of style isolation AND customisation options, here are a few aspects to consider. 

# Style isolation level

Technical solution available:
- **side-by-side integrator/widget code**. _A few CSS tricks will be needed to avoid unwanted style conflicts. This approach will be looked into closer bellow._
- **iframe** 
- **shadow DOM**

# Style customisation level

This is actually a range, that can go from:
1. _Little to no customisation_
2. _Specific allowed customisation_
3. _Anything can be customised_.


Technical solutions available for allowing style customisation:
- shared CSS (when side-by-side integrator/widget)
- definition of CSS variables
- style passed to the widget, as a configuration, or a file: 
  - passed in-process at initialisation
  - passed using iframe `postMessage`
  - passed via HTTP retrieval, based on a unique integrator id
- complete customisation, by allowing the integrator to fork your widget source code (if your widget is open source)

**Conclusion**: Depending on your needs and choice, the best solution will not be the same for everyone.

Let's see a few of these approaches in more detail in the next sections.  

_NOTE_: There is a code sandbox at the end.

# Approach 1: wrap your widget with an `iframe`

This solution is very effective, since it creates a new context. CSS style outside the iframe will not influence your widget.

You then have many options to define your customisation interface, while keeping the rest of the widget style immune to style collisions.

Important remark: there are 2 ways to add an iframe to your widget:
- add it yourself, inside the widget code. This applies it to all the integrators. 
- tell the integrators to add the iframe around the widget. This way, the integrator creates an isolation between their style, and the widget style. They can then apply custom CSS, the same way as the side-by-side approach:
```html
<div class="widgetWrapperToOverrideWidgetStyle">
  <div class="widgetRoot">Injected widget</div>
</div>
```

This approach is one of the preferred ones when you want to avoid style collisions at all cost. 

# Approach 2: Side-by-side integrator/widget, with `CSS unset` isolation

This solution may be enough in many cases. But it comes with some drawbacks to the widget:
- it resets the browser defaults (example a `div` will no longer have `display: block`, but `display: inline`). 
So extra style is required to restore that. In theory, many properties may be impacted. In my experience, setting `display: block` is enough.
- some unintended style override is still possible. This happens if high selectivity CSS is used on the integrator side, such as `!important`.

However, this last point is also an advantage, since it allows the integrator to customise anything, if needed.

Useful CSS concepts related to this approach:
- [all](https://developer.mozilla.org/en-US/docs/Web/CSS/all)
- [unset](https://developer.mozilla.org/en-US/docs/Web/CSS/unset)
- [inherited properties](https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance#inherited_properties)

In order to make this work well, you need to be careful not to pollute the style of the integrator elements. This can be avoided with discipline in the kind of CSS the widget uses.

What you need to avoid:
- `*` selector 
- element selectors (examples: `div`, `h1`)
- selectors with low specificity, using common names (example: `.button`). 

All these can be made safe by using them within a higher selectivity selector, such as: `#widgetRoot *`, `#widgetRoot div`, `#widgetRoot .button`.


Extract of the implementation of this approach:
```css
/* this is the main trick */
.widgetRoot * {
  all: unset;
}

/* Compensate unwanted effect of `unset` */
.widgetRoot,
.widgetRoot * {
  margin: 0;
  padding: 0;
  border: 0;
}
.widgetRoot div,
.widgetRoot h1 {
  display: block;
}
```

With the CSS trick described above, you now have more isolation between the integrator style and the widget style. Integrator style should not, by default, have unexpected impact on the widget style.

It is still possible to customise the widget style, but it requires deliberate intention, since it requires high selectivity (for instance use of id selector). Example:
```css
#integratorRoot .widgetRoot {
  --color-theme: pink;
}
```
Depending on the selector approach used inside your widget, there are 2 scenarios:
- stable widget selectors (example `.widgetTitle`, `.widgetFooter`). This does allow the integrator to reliably override the style of these elements. 
- unstable widget selectors (example: generated with file content hash, `.rDhk_jr_i8`). This can be done on purpose, to signal the integrator that they should not attempt to customise other things other than the customisation interface that you defined, such as your CSS variables). 

In conclusion, this solution making use of `all: unset` may work well in some cases. But because of its _soft_ type of boundary, you may still encounter some issues with accidental CSS style overrides. And, you will need to write more CSS to compensate for the `unset` effect, and avoid CSS conflicts.

So if you want to play safe, then _harder_  isolation context approaches are probably preferable.

# Approach 3: Shadow DOM 

I don't have experience with this approach, and I didn't look into it at this point[_time constraint_]. However, the described use-case is probably well-suited for what shadow DOM has to offer (style encapsulation).

I will leave it to you to read more about it: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM

# Code demo

I implemented a quick demonstration for Approach 1 and 2 side-by-side. Feel free to explore the code.
{% codesandbox https://codesandbox.io/embed/dazzling-sky-5qqcfv?fontsize=14&hidenavigation=1&theme=dark %}

_Disclaimers_: 
- The shared demo does not contain production-quality code. 
- The approaches that are listed here don't represent an exhaustive list of all possible approaches. Feel free to suggest others that I missed.