Shadow DOM in Ionic

Shadow DOM Usage in Ionic Web Components



·

Beginning with Ionic 4, all of Ionic’s components will be web components. Unlike previous iterations of the framework where the components were built using Angular, the move to web components means Ionic can now be used just about anywhere.

One aspect of web components is the ability to define custom HTML elements, which basically means that the Ionic team can create their own <ion-list> element that the browser will recognise and support as a web component. Another aspect of web components is the option to use a Shadow DOM.

Although many of Ionic’s web components will implement a Shadow DOM, it is important to note that this will not drastically affect the way you create applications with Ionic. As with most of the changes in Ionic 4, the changes that have been implemented mostly take place behind the scenes, and the syntax for your applications will largely remain the same. The biggest change with the introduction of Shadow DOM is that if you want to modify the internal structure of any of Ionic’s components that implement Shadow DOM, you will need to do so using CSS4 variables (whereas previously you could just target elements of the component with standard CSS selectors).

It is not important to understand the specifics of how Shadow DOM works in order to use Ionic, but a surface level understanding might help stop you from running into a brick wall.

We’re going to expand on this concept a lot throughout this article (so don’t worry if you don’t get all the terminology), but let’s start with a basic understanding of Shadow DOM. The general concept of Shadow DOM is to give web components their own little world to operate in within a website/application. DOM refers to the Document Object Model which represents the structure of the HTML nodes in a website (i.e. how your HTML elements are nested within each other), and a Shadow DOM is another isolated DOM for a specific web component within the main DOM.

Shadow DOM in Ionic Diagram

A web component using Shadow DOM will be inside of the DOM for your web page like any other element, except that it gets its own DOM to work with. This means that the web component can operate without worrying about being affected by any global styles or scripts that are being applied to the website. Likewise, we don’t have to worry about the internals of the web component messing with other parts of our websites or applications.

Since Web Components/Shadow DOM is native to the browser, the heavy lifting to provide this isolation can be performed by the browser directly, rather than having to ship extra JavaScript to do the work.

In my mind at least, I think it is easy to think of this concept as web component isolation. The main goal is to make web components completely self-contained and immune to outside interference. With a web component that utilises Shadow DOM, you can be reasonably sure it will work and be displayed as intended when you add it, as any existing styling or scripting in the application will not impact the web component.

We are going to talk through a lot of theory in this article to understand what is really going on, but I have summarised the important points to remember below.

Key points:

  • You cannot style any of the internal elements of a web component from outside of the web component using CSS selectors
  • You can style the internal elements of a web component if CSS4 variables are being used, as you can change the values of the CSS4 variables.
  • You can style slotted content inside of a web component (i.e. content you have supplied to the web component) from outside of the web component
  • You can style the host element of the web component (i.e. the element you use to add the web component to a page) from outside of the web component

Shadow DOM Usage in Ionic

With a basic understanding of the purpose of a Shadow DOM, let’s get into the specifics of how it is utilised in Ionic. Since Ionic 4, every one of Ionic’s components is a web component, and they can utilise their own Shadow DOM.

If we were to create a basic template in an Ionic application that looks like this:

<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>

  <ion-list>
    <ion-item>
      <p>Hello there.</p>
    </ion-item>
  </ion-list>

</ion-content>

Each of the elements you see above is a web component (since we are exclusively using Ionic components), and each of those web components can have their own Shadow DOM.

If we consider <ion-item> as an example, we might try to apply some styles to it from the page that is using the component:

/* No effect */
.item-inner {
    display: none;
}

/* Hides ion-item */
ion-item {
    display: none;
}

In this example, the top selector is targeting an element with a class of item-inner – which is an internal element of the <ion-item> web component (you can see this by inspecting the element in the debugger). Since the element we are targeting is inside of the Shadow DOM, we can’t style it with CSS from anywhere outside of that Shadow DOM, and so this style will have no effect.

However, we can still target the host element of the web component. If we were to target the <ion-item> itself, we can still apply styles to that – we just can’t access the internals of the web component.

If you were to take a peek in the debugging tools, you would see that the structure of the page looks like this:

Shadow DOM Screenshot

The two main things that you might notice are that:

  1. There are a lot more elements in the DOM than we added to our template
  2. There are these strange references to #shadow-root (open) scattered about the place.

The first point is explained by the fact that we are embedding a web component that comes with its own functionality. We might just add <ion-item> but the Ionic team have built a whole lot more into those custom web components that will be injected into the page (like the element with a class of item-inner that we tried to target).

So, what’s the deal with “shadow root”? A shadow root is the root node of the Shadow DOM tree, the element that this shadow root is attached to is called the shadow host (i.e. the element hosting the Shadow DOM). Wherever you see #shadow-root you can click to expand it, and what you see inside of #shadow-root is the Shadow DOM that is isolated from the rest of the document.

Shadow DOM Screenshot

Notice that in this example where we have the Shadow DOM for <ion-item> expanded we can see all of its contents, but if we close the shadow root…

Shadow DOM Screenshot

We can see that our <p> tag and its content is sitting outside of the Shadow DOM, even though we put that paragraph inside of <ion-item>. With the shadow root expanded, you will notice that all of the elements inside of the Shadow DOM have a slightly gray background. Our paragraph tag should be inside of this gray area that is the Shadow DOM, but if we look at the section of the Shadow DOM where the paragraph tag is you will see it just has this little arrow and the text reveal:

Shadow DOM Screenshot

Our paragraph tag isn’t part of the web component itself, it is just projected there using something called “slots”.

I’ve already written a detailed article about how slots are used in Ionic, so if you haven’t already I would recommend reading: Understanding How Slots are Used in Ionic 4. In short, the author of the web component (Ionic in this instance) can add “slots” to the web component that will allow content to be passed into it. Content that is added between the web components tags (e.g. between <ion-item></ion-item>) can be injected into the appropriate place inside of the web component.

The important thing to note about “slotted content” is that we are still able to style it from outside of the Shadow DOM. If we were to add another rule to our CSS:

/* No effect */
.item-inner {
    display: none;
}

/* Hides ion-item
ion-item {
    display: none;
} */

p {
    color: red
}

It would make the paragraph text red, even though that paragraph is being displayed inside of the Shadow DOM.

Styling with the Shadow DOM in Ionic

Having the internal elements of web components we use be isolated from outside interference is great… but sometimes we will want to change the styles of elements contained within the web component.

We can inject content into web components through the use of slots, and we can style that content, but what if we want to change a style on some particular element within the web component itself? Surely there is some way we can brute force our way in there, right?

Well, there isn’t really (at least, there isn’t a way to do it “properly”). We’re not completely out of luck, though. Ionic web components make heavy use of CSS4 variables – if you are not already familiar with CSS4 variables I would recommend reading: A Primer on CSS 4 Variables for Ionic 4

Since Ionic web components base a lot of their styles on the values of CSS4 variables, we can change the values of those variables to modify the internal styling of the web component. For example, let’s take a look at some of the CSS4 variables that the <ion-item> web component utilises:

--ion-color-base: var(--ion-item-background, transparent);
--ion-color-contrast: var(--ion-item-text-color, var(--ion-text-color, #000));
--ion-color-tint: var(--ion-item-background-color-active, #f1f1f1);
--ion-color-shade: rgba(var(--ion-item-border-color-rgb, 0, 0, 0), 0.13);
--transition: background-color 300ms cubic-bezier(.4, 0, .2, 1);
--padding-start: 16px;
--inner-padding-end: 8px;
--padding-start: 16px;

These are just a few of the variables being used, you will find that there is a whole lot more. Therefore, we can change just about any variable we want and have those changes be reflected inside of the Shadow DOM.

The problem is that whilst these variables exist for you to control, you might not know what ones you need to use. I’m not sure how the documentation for this will end up shaping up exactly, but it is easy enough to figure this out for yourself.

Open the browser debugging tools – just right-click and go to Inspect Element on the element that you are interested in styling – and then navigate to the element you are interested in styling (even if it is inside of a Shadow DOM). Open up the ‘Styles’ on the right-hand side and filter for --:

Shadow DOM Screenshot

This will highlight all of the variables that are affecting the styling of the elements. You can tinker with the variables right there in the debugger and see the changes take effect right away. Once you have figured out the variable that you want to change you can just make the change permanent by adding it to your SCSS/CSS file.

Since CSS4 variables cascade like any other CSS rules, you could make the variable specific to the <ion-item> component on a specific page:

home.page.scss

ion-item {
    --ion-item-background-color-active: #000;
    --min-height: 100px;
}

or if you wanted to you could even make this variable change globally by adding it to the :root selector (which is found in theme/variables.scss for Ionic/Angular applications, or you can just create a new :root selector wherever you like):

:root {
    --ion-item-background-color-active: #000;
}

This would then change that variable for every single component being used in your application (so you usually don’t want to do this except for the base theme colours).

Impact of Shadow DOM on JavaScript

Most of the stuff we have been talking about has related to the impact of Shadow DOM on CSS and styling. However, it is important to note that these isolation concepts also extend to JavaScript. If you were to try and grab an element that is inside of the Shadow DOM with:

element.querySelector()

It would not work. I won’t be spending any time talking about this in this article because I don’t think it is something that is going to impact most people. It is important to keep in mind, though, and it is something that impacted the creation of the scroll/vanish component that I wrote about recently (e.g. when listening for changes with the Mutation Observer API we had to make sure to listen to the shadowRoot, not the element itself).

Summary

The introduction of Shadow DOM for Ionic web components is going to be a great thing as it will definitely reduce a great number of bugs/issues. Shadow DOM essentially makes it “safe” to drop a web component anywhere, without worrying that existing styling/scripting is going to mess it up.

It is, however, something that is likely to trip people up for some time, as the concepts of a Shadow DOM and CSS variables are something that is still very new to many developers.

What to watch next...