DEV Community

Manuel Micu
Manuel Micu

Posted on

Avoiding CSS style collisions when building a UI widget

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:
<div class="widgetWrapperToOverrideWidgetStyle">
  <div class="widgetRoot">Injected widget</div>
</div>
Enter fullscreen mode Exit fullscreen mode

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:

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:

/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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:

#integratorRoot .widgetRoot {
  --color-theme: pink;
}
Enter fullscreen mode Exit fullscreen mode

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.

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.

Top comments (0)