- Framework-independent standalone HTML serializer for generating markup for Web Components.
- Expand any HTML element (including custom elements and web components) to HTML with defined conventions from web standards.
- This means that Web Components created with WebC are compatible with server-side rendering (without duplicating author-written markup).
- Compilation tools to aggregate component-level assets (CSS or JS) for critical CSS or client JavaScript.
- Opt-in to scope your component CSS using WebC’s built-in CSS prefixer.
- Or, use browser-native Shadow DOM style scoping (for future-compatibility when Declarative Shadow DOM browser support is ubiquitous)
- Progressive-enhancement friendly.
- Streaming friendly.
- Shadow DOM friendly.
- Async friendly.
- The
.webc
file extension is recommended (not a requirement)—you can use.html
.- Tip for Visual Studio Code users: go to
Preferences -> Settings -> Files: Associations
to add a mapping for*.webc
tohtml
.
- Tip for Visual Studio Code users: go to
@11ty/eleventy-plugin-webc
adds WebC to Eleventyexpress-webc
by @NickColley adds WebC to Express
“javascript frameworks are dead to me”—Andy Bell
“The DX and authoring model you landed on here looks fantastic”—Addy Osmani
“Really like the programmatic API approach over using a bundler to pre-compile and then serve.”—Harminder Virk
Folks doing similar things with Web Components: check them out!
Note: if you’re not building a plugin or integration for WebC, you can probably skip this section!
It’s available on npm as @11ty/webc
:
npm install @11ty/webc
This is an ESM project and as such requires a "type": "module"
in your package.json
(or use the .mjs
file extension).
import { WebC } from "@11ty/webc";
You can use this in a CommonJS file via dynamic import:
(async function() {
const { WebC } = await import("@11ty/webc");
})();
import { WebC } from "@11ty/webc";
let page = new WebC();
// This enables aggregation of CSS and JS
// As of 0.4.0+ this is disabled by default
page.setBundlerMode(true);
// File
page.setInputPath("page.webc");
// Or, a String
// page.setContent(`<p>Hello!</p>`);
let { html, css, js, components } = await page.compile();
// Or, Readable Streams for each
let { html, css, js } = await page.stream();
If WebC looks familiar, that’s because WebC is HTML. These are single file HTML components but don’t require any special element conventions (for example Vue’s single file component uses a top-level <template>
for markup). Using <template>
in a WebC file will output 👀 a <template>
element.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebC Example</title>
</head>
<body>
WebC *is* HTML.
</body>
</html>
- Uses
parse5
to parse WebC HTML as modern browsers do (credit to @DasSurma’s work with Vite here) <!doctype html>
is optional (will be added automatically if the content starts with<html
).- Throws a helpful error if encounters quirks mode markup.
To use components, we provide a few options: registering them globally via JavaScript or dynamically declaratively importing directly in your WebC file via webc:import
.
import { WebC } from "@11ty/webc";
let page = new WebC();
// Pass in a glob, using the file name as component name
page.defineComponents("components/**.webc");
// Array of file names, using file name as component name
page.defineComponents(["components/my-component.webc"]);
// Object maps component name to file name
page.defineComponents({
"my-component": "components/my-component.webc"
});
And now you can use them in your WebC files without importing!
Consider this page.webc
file:
<!doctype html>
<title>WebC Example</title>
<my-component></my-component>
When compiled, this will expand <my-component>
to include the contents inside of components/my-component.webc
.
If the components/my-component.webc
file contains:
Components don’t need a root element, y’all.
Compiling page.webc
will return the following HTML:
<!doctype html>
<html>
<head>
<title>WebC Example</title>
</head>
<body>
Components don’t need a root element, y’all.
</body>
</html>
Tricky trick: you aren’t limited to custom element names (e.g. my-component
) here. You can use p
, blockquote
, h1
, or any tag name to remap any HTML element globally. A more useful example might be an img
component that uses the Eleventy Image utility to optimize all images in your project.
Registering global components is not required! You can use webc:import
to dynamically import another component inline.
Important note: webc:
attributes are always removed from the resulting compiled markup.
page.webc
:
<!doctype html>
<title>WebC Example</title>
<any-tag-name webc:import="components/my-component.webc"></any-tag-name>
Another important note: We check for circular component dependencies and throw an error as expected if one is encountered.
New in WebC v0.6.2, you can import directly from an installed npm package. Here’s an example using a WebC component supplied by the Eleventy Syntax Highlighter plugin (4.2.0
or newer):
<syntax-highlight language="js" webc:import="npm:@11ty/eleventy-plugin-syntaxhighlight">
function myFunction() {
return true;
}
</syntax-highlight>
This uses the component tag name (syntax-highlight
) to look for a WebC component at node_modules/@11ty/eleventy-plugin-syntaxhighlight/syntax-highlight.webc
and imports it for use on this node. This works with a tag name override via webc:is
too.
Use webc:is
to remap a component to something else!
<div webc:is="my-component"></div>
<!-- equivalent to -->
<my-component></my-component>
All of the examples we’ve used so far were HTML-only components. You’ll note that when a component is HTML-only, it replaces the host component with the component content (<my-component>
was left out of the output).
If you’d like to keep the host component element around, use webc:keep
:
<my-component webc:keep></my-component>
Compiles to:
<my-component>Components don’t need a root element, y’all.</my-component>
Adding a <style>
or <script>
element to your component file will automatically keep the host component tag around too (for styling or scripting purposes). You can opt-out of this using webc:nokeep
.
Child content optionally precompiles using <slot>
and [slot]
too.
page.webc
:
<my-component>This is the default slot</my-component>
components/my-component.webc
:
<p><slot></slot></p>
Compiles to:
<p>This is the default slot.</p>
Important note: per web component standard conventions, if your component file contains no content markup (for example, only <style>
or <script>
), <slot></slot>
is implied and the default slot content will be included automatically. If the WebC component file does contain content markup, the content passed in as the default slot requires <slot>
to be included.
This works with named slots too:
page.webc
:
<my-component>
This is the default slot.
<strong slot="named-slot">This is a named slot</strong>
This is also the default slot.
</my-component>
components/my-component.webc
:
<p><slot name="named-slot"></slot></p>
Compiles to:
<p><strong>This is a named slot.</strong></p>
If your WebC component wants to output a <slot>
in the compiled markup for use in clientside JavaScript, use the aforementioned webc:keep
attribute (e.g. <slot webc:keep>
).
Enabling (off-by-default) Bundler Mode (page.setBundlerMode(true)
) aggregates CSS and JS found in WebC components.
As noted in the JavaScript API section above, the compile
method returns four different properties:
page.setBundlerMode(true);
let { html, css, js, components } = await page.compile();
By default, <style>
and <script>
elements in component files are removed from individual component markup and aggregated together for re-use elsewhere (you could write this to a file, or use as Critical CSS in another layout template—the Eleventy plugin will smooth this over for you). This includes <link rel="stylesheet">
and <script src>
when the URLs point to files on the file system (remote URL sources are not yet supported).
Note that if a <style>
is nested inside of declarative shadow root template (e.g. <template shadowroot>
), it is also left as is and not aggregated.
You can also opt out of aggregation on a per-element basis using <style webc:keep>
or <script webc:keep>
.
page.webc
:
<my-component>Default slot</my-component>
components/my-component.webc
:
<style>
my-component {
color: rebeccapurple;
}
</style>
Compilation results:
page.setBundlerMode(true);
let results = await page.compile();
// `results`:
{
html: "<my-component>Default slot</my-component>",
css: ["my-component { color: rebeccapurple; }"],
js: [],
components: ["page.webc", "components/my-component.webc"]
}
The order of aggregated styles and scripts is based on the dependency graph of the components in play (the order is noted in the components
array, a list of component file names).
We include a lightweight mechanism (webc:scoped
) to scope component CSS. Selectors will be prefixed with a new component class name hash key based on the style content. If you use :host
here it will be replaced with the class selector.
page.webc
:
<my-component>Default slot</my-component>
components/my-component.webc
:
<style webc:scoped>
:host {
color: blue;
}
:host:defined {
color: rebeccapurple;
}
</style>
Compilation results:
page.setBundlerMode(true);
let results = await page.compile();
// `results` (js and components omitted):
{
html: "<my-component class=\"wcl2xedjk\">Default slot</my-component>",
css: [".wcl2xedjk{color:blue}.wcl2xedjk:defined{color:rebeccapurple}"],
}
You can also specify an attribute value to webc:scoped
to hard code your own component prefix (e.g. <style webc:scoped="my-prefix">
). This allows the CSS to look a bit more friendly and readable. We will automatically check for duplicate values in your component tree and throw an error if collisions occur.
Note: Some folks recommend using Declarative Shadow DOM (the fastest and safest way to do component style encapsulation), however (in my personal opinion) the JavaScript progressive enhancement story there requires ubiquitous browser support before using it for content in the critical rendering path (so just be aware). You can use both methods in WebC!
You can also transform individual element content using the setTransform
method.
let component = new WebC();
let md = new MarkdownIt({ html: true });
component.setTransform("md", async (content) => {
// async-friendly
return md.render(content);
});
Now you can automatically transform markdown in your WebC templates via the webc:type
attribute.
<template webc:type="md">
# Header
</template>
Compiles to:
<h1>Header</h1>
- Bonus feature:
webc:type
supports a comma separated list of transforms.
Note that the <template webc:type>
node is compiled away. If you’d like to keep it around, use webc:keep
(e.g. <template webc:type webc:keep>
).
We do provide two built-in transforms in WebC: JavaScript Render Functions (webc:type="render"
) and CSS scoping (webc:scoped
). Those are covered in separate sections. You can override these with the setTransform
API but it is generally recommended to add your own named transform!
(WebC v0.7.1+)
Use webc:if
to conditionally render elements. Accepts arbitrary JavaScript (and is async-friendly). Similar to dynamic attributes, this also has access to component attributes and properties.
<div webc:if="true">This will render</div>
<div webc:if="false">This will not render</div>
<div webc:if="myAsyncHelper()">If the helper promise resolves to a truthy value, this will render</div>
For more complex conditionals, webc:type="js"
(WebC v0.7.1+) is recommended (read more below).
Consider this example:
page.webc
:
<my-component class="sr-only">This is the default slot</my-component>
Inside of your component definition, you can add attributes to the host component using webc:root
components/my-component.webc
:
<template webc:root class="another-class">
Some component content
</template>
class
andstyle
attributes are merged as expected.
Make any attribute into a dynamic attribute by prefixing it with a :
. You have access to host component attribute and property names (as well as page data) here!
page.webc
:
<avatar-image src="my-image.jpeg" alt="Zach is documenting this project"></avatar-image>
components/avatar-image.webc
:
<img :src="src" :alt="alt" class="avatar-image">
- In the HTML specification, attribute names are lower-case. When referencing these inside of a dynamic attribute, use the lower-case name (e.g.
<avatar-image mySrc="test">
would be:src="mysrc"
). See issue #71 for more discussion.
Make any attribute into a prop by prefixing it with @
. Props are “private” attributes that don’t end up in the output HTML (they are private to WebC). They are identical to attributes except that they are filtered from the output HTML.
page.webc
:
<avatar-image src="my-image.jpeg" alt="Zach is documenting this project" @secret="This is just between us"></avatar-image>
components/avatar-image.webc
:
<img :has-a-secret="secret ? true : false" :alt="alt" :src="src">
<!-- outputs <img has-a-secret="true" …> -->
- In the HTML specification, attribute names are lower-case. When referencing these inside of a dynamic attribute, use the lower-case name (e.g.
<avatar-image @mySrc="test">
would be:src="mysrc"
). See issue #71 for more discussion.
You can also transform individual element content using webc:type
. There are three built-in types:
webc:type="js"
which supercedeswebc:type="render"
webc:type="css:scoped"
(internal forwebc:scoped
—overridable!)
JavaScript Render Functions are async friendly (e.g. async function()
):
Run any arbitrary server JavaScript in WebC. Outputs the result of the very last statement executed in the script. Async-friendly (return a promise and we’ll resolve it).
page.webc
:
<img src="my-image.jpeg" alt="An excited Zach is trying to finish this documentation">
components/img.webc
:
<script webc:type="js">
if(!alt) {
throw new Error("oh no you didn’t");
}
`<img src="${src}" alt="${alt}">`;
</script>
Expand to see this example with webc:type="render"
<script webc:type="render">
function() {
if(!this.alt) {
throw new Error("oh no you didn’t");
}
// Free idea: use the Eleventy Image plugin to return optimized markup
return `<img src="${this.src}" alt="${this.alt}">`;
}
</script>
Or use a JavaScript render function to generate some CSS:
page.webc
:
<add-banner-to-css license="MIT licensed">
/* Some other CSS content */
</add-banner-to-css>
components/add-banner-to-css.webc
:
<script webc:type="js" webc:is="style">`/* ${license} */`</script>
<slot></slot>
Expand to see this example with webc:type="render"
<script webc:type="render" webc:is="style">
function() {
return `/* ${this.license} */`;
}
</script>
<slot></slot>
(Yes you can use <script webc:type="js" webc:scoped>
too).
Here’s another example of a more complex conditional (you can also use webc:if
!):
<script webc:type="js">
if(alt) {
`<img src="${src}" alt="${alt}">`
} else {
`<a href="${src}">Your image didn’t have an alt so you get this link instead.</a>`
}
</script>
Note that you have access to the component attributes and properties in the render function (which is covered in another section!).
We provide a special @html
property to override any tag content with custom JavaScript.
<template @html="'Template HTML'"></template>
<template @html="dataProperty"></template>
<!-- webc:nokeep will replace the outer element -->
<template @html="'Template HTML'" webc:nokeep></template>
- Using
webc:raw
will prevent reprocessing the result as WebC. (v0.6.0+) - Use
@raw
as an alias forwebc:raw @html
. (v0.7.1+)
We provide a special @text
property to override any tag content with custom JavaScript. The entire value returned here will be escaped!
<p @text="dataProperty"></p>
<!-- When dataProperty contains `<p>This is text</p>`, this renders: -->
<p><p>This is text</p></p>
<!-- webc:nokeep will replace the outer element -->
<p @text="dataProperty" webc:nokeep></p>
If you want to add custom JavaScript functions for use in render functions, @html
, or dynamic attributes you can use the setHelper
method.
import { WebC } from "@11ty/webc";
let page = new WebC();
page.setHelper("alwaysBlue", () => {
return "Blue"
});
And this.alwaysBlue()
is now available:
<script webc:type="render" webc:is="template">
function() {
return this.alwaysBlue();
}
</script>
Opt out of WebC template processing using webc:raw
. This works well with <template>
content. See also the special @raw
content property (WebC v0.7.1+)
<template webc:raw>
Leave me out of this.
<style>
p { color: rebeccapurple; }
</style>
</template>
Custom elements (per specification) are not supported as void elements: they require both a starting and ending tag. You can workaround this limitation using webc:is
.
There are a few wrinkles when using an HTML parser with custom elements. Notably, the parser tries to force custom element children in the <head>
over to the <body>
. To workaround this limitation, use webc:is
. Here are a few example workarounds:
<head webc:is="my-custom-head">
<!-- this is slot content, yes you can use named slots here too -->
</head>
<head>
<!-- <my-custom-head> is not allowed here -->
<meta webc:is="my-custom-head">
<title webc:is="my-custom-title">Default Title</title>
</head>
There are two different rendering modes in Eleventy: page
and component
. We attempt to guess the rendering mode that you’d like based on the markup you supply. The page
rendering mode is for rendering full HTML pages. The component
rendering mode is for fragments of HTML. Most of the time you won’t need to worry about this distinction but it is included in the documentation for completeness.
page
is used when the markup starts with<!doctype
(or<!DOCTYPE
) or<html
(WebC forces no-quirks parsing).component
is used otherwise.