SJ (Scott Jehl)

HTML Web Components on the Server Are Great

photo of scott jehl profile headshot

By Scott Jehl

I'll start this post by echoing what many others have been saying: Web Components are having a moment lately! After a decade dominated by fragile, complicated, and slow Single-Page Application delivery patterns, it's refreshing to see folks stepping back to look for simple, resilient, standards-based alternatives... and discovering Web Components. Meanwhile, there's an understandable "what took so long?" sentiment coming from many folks who have been happily using web components for a long time.

That's even more true of "HTML Web Components," a new term describing a judicious use of a subset of Web Components' feature set, since that subset has been supported across browsers for many years. Better late than never though! The increased interest in this pattern is great for all of us.

In this post, I want to talk about some tools that enable HTML Web Component patterns to work on the server-side, making it easier to use them at scale in sites and apps of all kinds.

First, What's an HTML Web Component?

Much like the term "Responsive Web Design," "HTML Web Components" refer to a pattern that use several web standards in a certain way to achieve a particular outcome. It's not my term (it's Jeremy's), but here's how I'd attempt to describe them.

They are custom elements that

  1. are not empty, and instead contain functional HTML from the start,
  2. receive some amount of progressive enhancement using the Web Components JavaScript lifecycle, and
  3. do not rely on that JavaScript to run for their basic content or functionality.

Perhaps a little code might help illustrate.

The following HTML Web Component uses a custom element called my-header to wrap a website's masthead markup, followed by a JavaScript module that enhances that custom element's interactivity:

<my-header>
  <header>
    <img src="logo.svg" alt="ACME Hammers">
    <nav>
      <a href="/">Home</a>
      <a href="/contact">Contact</a>
    </nav>
  </header>
</my-header>

<script type="module">
class MyHeader extends HTMLElement {
  constructor() {
    super();
  }
  connectedCallback() {
    console.log("my-header element added to page. Here we can turn that nav element into a burger menu");
  }
}
customElements.define("my-header", MyHeader);
</script>

I've paraphrased the enhancement part in a console.log above to save space, but the HTML content and JS scaffolding's all there. Most importantly, the HTML contains markup that will be usable regardless of whether the script successfully runs or not. If the script does run though, the browser will automatically recognize every my-header it finds as an element with inherent custom behaviors attached, just like the other HTML elements you use. For examples of ready-to-use HTML Web Components, I'd recommend browsing that section of Zach Leatherman's Taxonomy of Web Components post.

Composability Assumptions & Hurdles

Once you have an HTML Web Component like this, you may want to place it in many pages on a site. Ideally, you might want to do that by putting that HTML into its own file so that you can include that file into pages while maintaining it in one place. Unfortunately, HTML alone doesn't really have an "include" feature like that though, so you might reach for a server-side tool like PHP's <?php include('my-header.php'); ?>, or a static site generator like Eleventy's {% include "my-header" %} to do the job.

To be sure, those are both fine options!

However, there are now several tools designed to perform tasks like this using ordinary HTML syntax instead. These tools focus heavily on composability, using custom elements as a server-side templating abstraction. And most importantly, they perform "Server Side Rendering" of HTML Web Components, allowing you to compose complex, dynamic pages from components that are delivered to the browser as meaningful, resilient HTML.

For example, using these tools, we could include a my-header element into a page simply by referencing its tag, like this:

<!DOCTYPE html>
<html>
  <head>...</head>
  <body>
    <my-header></my-header>
    ...more page content here
  </body>
</html>

That alone would cause the my-header to expand in place with the full HTML defined in its file, before it is sent to the browser.

This is pretty neat stuff, I think! Let's talk about tools that make this happen and what else they can do.

Enter Custom Element SSR Tools!

To my knowledge, the first tool designed for this task was Enhance, built by the folks at Begin, and soon after that came Zach Leatherman's WebC. Nowdays, several frameworks and libraries make use of this kind of pattern, so there's a lot to choose from depending on your development stack.

As I've written recently, I've been consulting with the Enhance team and I'm most familiar with their framework, so I'll be using Enhance in this post's examples. That said, I'd encourage you to check out all the various flavors of tools that work with HTML Web Components to see which ones suit your needs.

Custom Element Composability in Enhance

Custom elements are a great abstraction for server-side templating. Let's look at some examples.

Say you have a page called about.html in the directory where the Enhance web server is running. The body of that file could look something like this:

<my-header></my-header>
<my-content></my-content>
<my-footer></my-footer>

This example references a few "elements" in Enhance parlance, which are literally files that live in the /elements/ directory. First, let's look at what the elements/my-header.mjs file might look like.

export default function MyHeader ({ html }) {
  return html`
    <my-header>
      <header>
        <img src="logo.svg" alt="ACME Hammers">
        <nav>
          <a href="/">Home</a>
          <a href="/contact">Contact</a>
        </nav>
      </header>
    </my-header>`
}

At a high level, my-header.mjs is a JavaScript module file, and what it does is export a function that returns a string of HTML. That string is the HTML for our my-header component, to be specific (note that I've removed the client-side script portion in the first example for brevity). Set up like this, as far as Enhance is concerned, it's a reusable HTML Web Component, and in about.html, that full HTML response above will replace the <my-header></my-header> element just like an old server-side include.

So that's a simple include, but it gets better.

Elements in Elements in Elements

Enhance doesn't stop at level 1 when it comes to including and expanding HTML elements. From here, you can do a lot more to compose views. For example, you can reference elements inside other elements, and they too will be expanded on the server-side for delivery. In Enhance, there's no limit to this depth of inclusion.

Perhaps you'd want to move the nav element above into its own custom element to include inside my-header. That's as easy as creating a my-nav.mjs element and replacing <nav> with <my-nav></my-nav> in the header file. And just so it's clear, just like standard custom elements these element names don't have any rules around the word "my" or anything like that. You might go with global-nav for example, or site-navigation-list and that's just as well.

Composing with Slots

Enhance takes custom elements much further than includes, of course. If you're familiar with web components, perhaps you've heard of the slot element. I won't cover it here more than saying that its a way to offer up a space inside a custom element that you can pass variable content into. Enhance supports the slots concept on the server side, enabling powerful nesting and composability.

Let's make use of a slot for the my-content element. Here's our about.html page again:

<my-header></my-header>
<my-content>
  <h1>About!</h1>
</my-content>
<my-footer></my-footer>

Do you see how we've made my-content into a sort of HTML Web Content of its own here? Now let's set up /elements/my-content.mjs to accept that h1 into a slot:

export default function MyContent ({ html }) {
  return html`
    <my-content>
      <main>
        <slot></slot>
      </main>
    </my-content>`
}

The HTML output that the browser receives from Enhance will look like this:

<my-content>
  <main>
    <h1>About!</h1>
  </main>
</my-content>

Ready to go! Of course, instead of just an h1 we could alternatively pass custom elements into the slot as well, and it all will be expanded for delivery just the same.

Taking things Dynamic

So far, these examples have been moving static HTML around, but given that Enhance elements live in server-side JavaScript modules, they can be as dynamic as you'd like. In Enhance, an element function merely needs to return a string of HTML, but you can compile that string from whatever you'd like.

Maybe the HTML string will incorporate response data from an API, or from the local application or session state. Maybe it needs to listen to attribute values passed into it from the parent tag. And additionally, perhaps you'll want to write some JavaScript logic to massage the output along the way.

All of these things are straight-forward within an enhance element. You can even return the response of a templating language, say, Mustache or something like that, if you happen to prefer its syntax. You just need to return a string of HTML.

As a simple example, let's finish our about page by using one dynamic variable inside the my-footer.mjs element to drive its copyright date:

export default function MyFooter ({ html }) {
  const year = new Date().getFullYear()
  return html`
    <my-footer>
      <footer>
        <p>Copyright ${ year } Scott Jehl</p>
      </footer>
    </my-footer>`
}

...which would neatly arrive in the browser as:

<my-footer>
  <footer>
    <p>Copyright 2024 Scott Jehl</p>
  </footer>
</my-footer>

No JavaScript required! (In the browser, that is.)

Wrapping Up

In this post, I've scratched the surface of how you can use a tool like Enhance to compose server-side templates using custom elements. To my eye, this represents a super-clean abstraction for constructing sites and applications. Additionally, for any elements that need further enhancement and interactivity on the client-side, the custom element output is a perfect foundation for extension using the client-side web component lifecycle (and Enhance offers helpers for that part too if you want them).

Thanks!

Thank you for reading. I hope you appreciate this pattern as much as I do and I'd encourage you to check out the Enhance site and docs from here!

Feel free to reach out on Mastodon or via the contact form if you have ideas or questions.