Skip to main content

All the ways to render a WebC component with 11ty

Watch the companion video to this post on YouTube

One of my favorite features of WebC is the variety of rendering options it provides. This allows full control to use WebC for HTML templating, custom elements, or web components (with or without the shadow DOM).

All of these rendering options offer some major benefits:

  • Explicit control over the final output for clean markup
  • Write HTML freely in components. There are no strange limitations, like Fragments in React to render multiple top-level elements.
  • Slots (with the <slot> element and slot attribute), including named slots, which alleviate the need for attributes (props) and offer even more control over the final output
  • Advanced templating with webc:if, webc:elseif, webc:else, and webc:for
  • Components can nest other components. WebC layouts are just components, so layouts can be nested as well.

Which rendering option to use

Each rendering option has different tradeoffs, so it can be difficult to know which to use for different situations.

Do you want basic HTML templating?

  • Simple HTML templating
  • Replace an element with the component's markup

Do you want scoped styles?

  • Overloading an HTML element
  • Scoped styles without using a custom element tag
  • Custom element

Do you want JavaScript?

  • Dynamic content with JavaScript render functions (server-side)
  • Custom element (server-side and global client-side)
  • Web component (client-side)

I prefer the least-powerful options based on my needs, and prioritize options that don't output a custom element tag. Custom element tags wrap around the component's internal markup, which can make layout and semantics more challenging in certain situations. This is somewhat alleviated with display: contents;, but I avoid it when possible.

Option 1: Simple HTML templating (the default)

The most common use case for components is HTML templating to reuse chunks of markup across a site, also known as partials or includes.

By default, WebC with 11ty allows you to write any markup you want in a component, use the component's custom element tag in your page, and it'll simply render the markup inside the component without the custom element tag.

In my-component.webc

<h2>Hello from the component</h2>

In page.webc

<my-component></my-component>

Rendered output at /page

<h2>Hello from the component</h2>
  • Benefits
    • Reuse markup
    • Doesn't render a custom element tag
  • Limitations
    • Can't use scoped styles
    • Can't use JavaScript

Option 2: Replace an element with the component's markup

This works identically to the default behavior, but allows you to replace any HTML element with a WebC component, without using the custom element tag.

This approach is required when rendering WebC components in the <head> element, since custom elements aren't allowed inside <head>.

Read 11ty's WebC documentation on <head> components

In my-component.webc

<h2>Hello from the component</h2>

In page.webc

<div webc:is="my-component"></div>

Rendered output at /page

<h2>Hello from the component</h2>
  • Benefits
    • Reuse markup
    • Works in the <head> element
    • Doesn't output a custom element tag
  • Limitations
    • Can't use scoped styles
    • Can't use JavaScript

Option 3: Overloading an HTML element

The custom element spec requires a hyphen in custom element names to avoid conflicts with native HTML elements. However, WebC intentionally allows for naming conflict to make "overloading" native HTML elements possible.

When a WebC component has the same name as a native HTML element, the component will replace any instance of that element.

This makes enhancements to native elements possible, such as always including an alt attribute on an <img> that defaults to an empty string.

This approach also allows for scoped styles without outputting a custom element tag. Using <style webc:scoped> in the .webc file will tie all styles to the component. 11ty and WebC do this by creating a hashed CSS class name that's added to the component's root-level element, such as <div class="weljrwcay">.

With scoped styles, it's possible to select the root-level element with the :host pseudo-selector. This follows the shadow DOM standard for selection a shadow root host, but in the case of WebC, it can be used for any scoped styles, regardless of creating an actual Web Component with a shadow DOM.

11ty will automatically aggregate any styles (scoped or global) it finds in WebC components. All that's needed is to include the CSS bundle in the base layout file:

<style @raw="getBundle('css')" webc:keep></style>

In footer.webc

<h2>Heading with red text color</h2>

<style webc:scoped>
	h2 {
		color: red;
	}
</style>

In page.webc

<h2>Heading with normal text color</h2>

<footer></footer>

Rendered output at /page

<h2>Heading with normal text color</h2>

<footer>
	<h2>Heading with red text color</h2>
</footer>
  • Benefits
    • Enhance native elements
    • Can use scoped styles
    • Doesn't output a custom element tag
  • Limitations
    • Must commit to overriding every instance of a native element
    • Can't use JavaScript

Option 4: Scoped styles without outputting a custom element tag

If you don't want to overload an HTML element but still want scoped styles, you can override the component's custom element tag with a native HTML element of your choice.

This is useful for creating variations of a common semantic element, such as <header> elements, without having a custom element tag getting in the way.

This approach makes use of the webc:root="override" attribute, which will replace the custom element tag.

In my-component.webc

<header webc:root="override">
	<p>Hello from the component</p>
</header>

<style webc:scoped>
	p {
		color: red;
	}
</style>

In page.webc

<my-component></my-component>

Rendered output at /page

<header>
	<p>Hello from the component</p>
</header>
  • Benefits
    • Don't have to commit to overloading a native element
    • Can use scoped styles
    • Doesn't output a custom element tag
  • Limitations
    • Can't use JavaScript

Option 5: Dynamic content with JavaScript render functions

Many components require more dynamic content, such as accessing data from 11ty's data cascade or adding timestamps.

This is achieved with JavaScript render functions, which run server JavaScript in a component. JavaScript render functions can be the entire component, or used within a component's markup just for the dynamic portions.

All it requires is a <script> tag in the .webc file with the webc:type="js" attribute. This render function outputs the last line of code, which is usually a template string of HTML.

Because this is server JavaScript, there won't be any JS bundling or client-side functionality.

In my-component.webc

<script webc:type="js">
	const currentDate = new Date();

	`<h2>Hello from ${currentDate.getFullYear()}</h2>`;
</script>

In page.webc

<my-component></my-component>

Rendered output at /page

<h2>Hello from 2023</h2>
  • Benefits
    • Can use server-side JavaScript
    • Allows access to 11ty's data cascade
    • Allows for more advanced templating, such as looping and variables
    • Doesn't output a custom element tag
  • Limitations
    • No client-side functionality
    • Can't add styles (global or scoped) within a render function
    • Have to author HTML in a template string (no syntax highlighting, autocomplete, linting, etc.)

Option 6: Custom element

By default, WebC will output the custom element tag any time there are scoped styles or JavaScript in a component.

Custom elements are mainly useful for advanced templating with webc:if, webc:elseif, webc:else, and webc:for that rely on JavaScript. Similar to JavaScript render functions, 11ty will run server JavaScript in a custom element component and statically render the output.

Because 11ty will bundle any styles and JavaScript it finds in WebC components, it's possible to add global client-JavaScript in a custom element component by simply including a <script> tag. But, this can be hard to maintain, so I wouldn't recommend it.

Custom element tags need to have a hyphenated name to avoid naming collision with native HTML elements, unless you're intentionally overloading an HTML element. Browsers handle custom element tags as a generic element, similar to a <div>.

Similar to styles, 11ty will automatically aggregate any client-side JavaScript it finds across WebC files into a bundle. If your .webc file includes any (global) client-side JavaScript, be sure to source the bundle in your base layout file:

<script type="module" @raw="getBundle('js')" webc:keep></script>

In my-component.webc

<h2>Hello from the component</h2>

In page.webc

<my-component></my-component>

Rendered output at /page

<my-component>
	<h2>Hello from the component</h2>
</my-component>
  • Benefits
    • Allows for more advanced templating while authoring normal HTML
    • Can use server-side or global client-side JavaScript
    • Can use scoped styles
  • Limitations
    • There are better options for scoped styles without outputting a custom element tag
    • Custom element tags may introduce some challenges with layout and semantics
    • Can't use scoped client-side JavaScript

Option 7: Web component

At last, we arrive at a full-on Web Component, with all its powers and quirks. I only reach for Web Components if I need client-side interactivity and functionality.

As with any client-side JavaScript, it's best to consider progressive enhancement in case JavaScript doesn't load or is disabled.

Additionally, WebC works with the 11ty is-land plugin for hydrating Web Components after the page has loaded. This layers interactivity on top of the statically-rendered content.

Because Web Components depend on client-side JavaScript, we need to source the JS bundle in our base layout file, if we haven't already:

	<script type="module" @raw="getBundle('js')" webc:keep></script>

The type="module" is technically only needed if a WebC component is an actual Web Component, but I always include it so I can freely upgrade any component to a full Web Component.

In my-component.webc

<button type="button">Log to the console</button>

<script>
	window.customElements.define("my-component", class extends HTMLElement {
		connectedCallback() {
			const button = this.querySelector(":scope button");

			button.addEventListener("click", () => {
				console.log("Hello from the component");
			});
		}
	});
</script>

In page.webc

<my-component></my-component>

Rendered output at /page

<!-- Logs "Hello from the component" to the console on click -->
<button type="button">Log to the console</button>
  • Benefits
    • Full Web Component spec
    • Light or Shadow DOM
    • Can use scoped styles
    • Can use scoped client-side JavaScript
  • Limitations
    • Custom element tags may introduce some challenges with layout and semantics
    • Need to think through progressive enhancement and hydration

Summary

These options can be overwhelming, but they provide complete control over the output from WebC components, which is a major win for styling, semantics, and accessibility. I find that WebC results in writing less code and create cleaner output than any framework I've used.

Resources

The official WebC documentation by 11ty details all the features, many of which I couldn't cover in this post.

Shout out to 11ty & WebC by W. Evan Sheehan has great articles that dig into the nuances and capabilities of WebC with helpful examples.