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 andslot
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
, andwebc: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.