Web Components  -  The Template-Viewport Pattern  for the Shadow DOM
Tags: webcomponents, html, template, htmlelement
Published: 2023-11-21 14:18:48

---
title: Web Components  -  The Template-Viewport Pattern  for the Shadow DOM
published: true
description: A guide about a useful pattern for writing Web Components which make use of the Shaddow DOM.
tags: webcomponents, html, template, htmlelement
# cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fc7rfsfc3gqklc4r8e1h.png
# Use a ratio of 100:42 for best results.
# published_at: 2022-11-08 06:55 +0000
---

Over the past couple of months, I've been working on a new no-code (HTML markup only) 'serverless' platform which uses native Web Components on the front end (see https://saasufy.com/).

One of the problems I ran into early on was related to styling components which make use of the Shadow DOM. For those who don't know, the Shadow DOM is a mechanism which allows a component to support a range of features; most notably, the ability to generate new child elements inside the component at runtime without polluting the main DOM and without conflicting with (or overwriting) child elements slotted in from the outside.

I had previously built components without using the Shadow DOM but I ran into a situation where I needed to work with slotted content/HTML whilst also generating additional HTML from within the component.

## The template

The component I wanted to build was a general-purpose `collection-browser` component; the goal of this component was to render a customizable, browsable list of elements by filling out an arbitrarily complex HTML template with data from a back end server for each entry in a collection. I realized that this could be achieved using the Shadow DOM with slotted `<template>` elements like this:

```html
<template slot="item">
  <div>Hello {{username}}!</div>
</template>
```

Unfortunately, upon rendering the filled-out template inside the Shadow DOM, external CSS definitions would not apply to them. This posed a significant problem because it meant that the `collection-browser` component could not be made 'general purpose' across multiple different projects and/or companies as it lacked styling flexibility.

The reason why external CSS styles do not apply to elements inside the Shadow DOM is because the Shadow DOM fully encapsulates its styling; this means that style definitions from the Shadow DOM cannot affect elements outside of it and it also means that elements which are inside the Shadow DOM cannot be styled with CSS that is defined outside of the component. This poses a significant problem when building general-purpose components.

While there are ways to 'inject' style information into the Shadow DOM from the outside, the approaches are unfamiliar and add complexity*. To be viable, the HTML generated by the `collection-browser` component had to abide by the page's main CSS styles automatically and without any magic.

This led me to the discovery of a simple pattern which I later re-used for many of other components. I call it the `template-viewport pattern`. The idea is that because HTML which is generated inside the Shadow DOM cannot be styled externally, any HTML generated from inside the component had to be injected outside of the Shadow DOM (inside the Light DOM).

## The viewport

The simplest solution I could find to allow this was to invent the concept of a 'viewport' element - This meant that the `collection-browser` component would need to accept two slotted elements:

- A template to use as input to generate some 'filled out' output HTML.
- A viewport element to act as a container for the  output HTML.

The idea behind this is that if a viewport element is slotted into my `collection-browser` component from the outside, any elements which are injected inside it (including those generated internally by the component) will abide by the page's main CSS styles as they will be part of the Light DOM and not the Shadow DOM.

Working with slotted elements with Web Components is simple. I defined a `render()` method for my component which generates placeholder tags for the slotted elements like this:

```js
this.shadowRoot.innerHTML = `
  <slot name="item"></slot>
  <slot name="viewport"></slot>
`;
```

Here, the slot with `name="item"` will hold a `<template slot="item">` element slotted in from the outside and the slot with `name="viewport"` will hold a slotted `<div slot="viewport"></div>` element.

Getting a reference to the Light DOM viewport element from inside the collection-browser via the Shadow DOM's `<slot name="viewport">` element is done like this:

```js
let viewportNode = this.shadowRoot
  .querySelector('slot[name="viewport"]').assignedNodes()[0];
```

Rendering some HTML inside it is just a matter of setting its innerHTML property like `viewportNode.innerHTML = ...`

## Components in action

Here's what the component looks like from outside:

```html
<!--
  Loads some chat messages from the server and renders
  each message based on the template with
  slot="item" into the slot="viewport" element near
  the bottom.
-->
<collection-browser
  collection-type="Chat"
  collection-fields="username,message,createdAt"
  collection-view="recentView"
  collection-view-params=""
  collection-page-size="50"
>
  <template slot="item">
    <div>
      <div class="chat-username">
        <b>{{Chat.username}}</b>
      </div>
      <div class="chat-message">{{Chat.message}}</div>
      <div class="chat-created-at">{{date(Chat.createdAt)}}</div>
    </div>
  </template>

  <div slot="viewport" class="chat-viewport"></div>
</collection-browser>
```

This construct has proven itself to be robust and works well with any amount of nesting. So for example you can have a `collection-browser` rendering a template which contains another `collection-browser` with its own template so it generates lists within a list.

Another good use case for this approach has been to build an `app-router` component which generates a different template based on the current URL's `location.hash`:

```html
<app-router>
  <template slot="page" route-path="/home">
    <div>This is the home page</div>
  </template>

  <template slot="page" route-path="/about-us">
    <div>This is the about-us page</div>
  </template>

  <template slot="page" route-path="/products">
    <div>This is the products page</div>
  </template>

  <div slot="viewport"></div>
</app-router>
```

Again, this works well with any level of nesting and you can mix and match different components.

**EDIT**

If you want to see this `collection-browser` and `app-router` code used in a working app, check out this GitHub repo: https://github.com/Saasufy/product-browser-demo/blob/main/index.html#L47-L80 - It can run anywhere (just Git clone) but if you're as lazy as me, you'll want to try the hosted version here: https://saasufy.github.io/product-browser-demo/index.html#/sport

The repo for the chat example I alluded to in this guide can be found here: https://github.com/Saasufy/chat-app and is hosted here: https://saasufy.github.io/chat-app/ (you can log in with GitHub by clicking on the link at the bottom of the log in form).

Anyway, that's it. I hope you find a use for this pattern in your own projects. Don't hesitate to hit the like button if you found this guide useful.

\* An approach to explicitly style elements inside a component's shadow DOM is by using the CSS parts API. See https://developer.mozilla.org/en-US/docs/Web/CSS/::part - This approach is ideal if you want to style inner elements but don't want them to abide by your page's style definitions.