Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/content/docs/api/cem-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ initializer and, for every key, records in the manifest:

- the **member** under its camelCase name
- the **attribute** under its kebab-case name, linked to that member
- the **type** inferred from the default value `boolean`, `number`, `object`
- the **type** inferred from the default value: `boolean`, `number`, `object`
or `string`
- the **default value** as written

Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/api/html.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ html`<p class="a">hi</p>`
Because the tree is a plain object it is comparable and serializable, which is
what lets `render()` diff one render against the next.

`` html`` `` returns `undefined` — this is the idiomatic way for a component to
`` html`` `` returns `undefined`. This is the idiomatic way for a component to
render nothing, and it empties the rendered subtree rather than leaving the
previous render on screen.

Expand All @@ -60,7 +60,7 @@ Each entry in `props` is applied by [`applyProp`](/api/utils/#applypropel-prop-v
in this order:

1. a `style` object is applied rule by rule
2. a name the element owns as a **DOM property** is assigned to that property
2. a name the element owns as a **DOM property** is assigned to that property,
so event handlers (`onclick=${fn}`) and non-string values keep their type
3. a boolean value with no matching DOM property is toggled as an HTML boolean
attribute
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/api/utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ everything else pass through unchanged.

### `deserialize(value, type)`

Parses an attribute string back into a value of the given declared type the
Parses an attribute string back into a value of the given declared type, the
inverse of `serialize()`.

| Parameter | Type | |
Expand All @@ -72,7 +72,7 @@ inverse of `serialize()`.
| `type` | `string` | `'boolean'`, `'number'`, `'object'`, `'undefined'` or `'string'` |
| **returns** | `any` | the parsed value |

`'boolean'` always returns `true` strict HTML boolean-attribute semantics,
`'boolean'` always returns `true`: strict HTML boolean-attribute semantics,
where any present value is true. Absence is handled by the caller and never
reaches here. `'number'`, `'object'` and `'undefined'` use `JSON.parse` and
throw on malformed input; strings pass through.
Expand Down
24 changes: 12 additions & 12 deletions docs/src/content/docs/api/web-component.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: WebComponent
slug: api/web-component
description: The WebComponent base classstatic configuration, instance members, lifecycle hooks and attribute converters.
description: 'The WebComponent base class: static configuration, instance members, lifecycle hooks and attribute converters.'
---

The base class every component extends. Import from the package root or its own
Expand Down Expand Up @@ -56,8 +56,8 @@ reported with `console.warn`:

| Default | Warning |
| ------------------ | --------------------------------------------------- |
| a function or symbol | not reflectable use handlers or refs instead |
| `true` | boolean defaults should be `false` invert the name |
| a function or symbol | not reflectable: use handlers or refs instead |
| `true` | boolean defaults should be `false`: invert the name |

A `true` boolean default is discouraged because HTML has no true-by-default
boolean attribute: absence would have to mean both "false" and "default". Name
Expand All @@ -80,7 +80,7 @@ per-component rules after it. Strings are compiled to a `CSSStyleSheet` once;
existing `CSSStyleSheet` instances are adopted as-is and can be shared across
components.

Adoption happens **once per instance**, when the element is constructed not
Adoption happens **once per instance**, when the element is constructed, not
per render.

Requires [`shadowRootInit`](#static-shadowrootinit). Without a shadow root
Expand All @@ -92,7 +92,7 @@ See it live: [Constructable styles demo ↗](https://demo.webcomponent.io/exampl
### `static shadowRootInit`

A [`ShadowRootInit`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options)
object. Its presence is what opts the component into shadow DOM — the shadow
object. Its presence is what opts the component into shadow DOM. The shadow
root is attached during construction and becomes the render target.

```js
Expand Down Expand Up @@ -143,9 +143,9 @@ Assigning the value it already holds does nothing.

Read-only getter returning what the component renders. Two kinds are supported:

- an [`html`](/api/html/) tagged template a vnode tree, reconciled in place
- an [`html`](/api/html/) tagged template: a vnode tree, reconciled in place
on re-render
- a **string** assigned to the render target's `innerHTML`
- a **string**: assigned to the render target's `innerHTML`

Both render into the same target: the shadow root when `shadowRootInit` is set,
the element itself otherwise. Returning `` html`` `` (which is `undefined`) or
Expand All @@ -165,7 +165,7 @@ Renders `template` into the render target. Called automatically on connect and
on every prop or attribute change; you rarely call it yourself.

For a vnode template, the new tree is compared against the previous one and
re-render **patches the existing DOM in place** — see
re-render **patches the existing DOM in place**. See
[Template vs Render](/template-vs-render/) for what that preserves and the
non-keyed matching caveat.

Expand Down Expand Up @@ -202,7 +202,7 @@ Override these to control how one prop crosses the prop/attribute boundary, and
call `super` for the props you do not handle.

The default conversion round-trips values through JSON. Types JSON cannot
restore `Date`, `Map`, `Set`, `URL`, class instances need overridden
restore (`Date`, `Map`, `Set`, `URL`, class instances) need overridden
converters to live on `static props`; see
[Custom attribute conversion](/prop-access/#custom-attribute-conversion) for
worked examples, including the non-serializable cases.
Expand All @@ -229,7 +229,7 @@ toAttribute(name, value) {

### `fromAttribute(name, value)`

Converts an attribute value into the prop value it represents the inverse of
Converts an attribute value into the prop value it represents, the inverse of
`toAttribute()`.

| Parameter | Type | Description |
Expand All @@ -256,7 +256,7 @@ Boolean props follow the HTML convention in both directions: **presence means
| `true` | present, empty value | `''` |
| `false` | absent | `null` |

Any present value reads as `true` including the literal `flag="false"`, just
Any present value reads as `true`, including the literal `flag="false"`, just
as native `disabled="false"` is still disabled. Removing the attribute always
yields `false`, never the declared default.

Expand All @@ -265,7 +265,7 @@ Use `toggleAttribute(name, bool)` to set them. Writing
when it sees a boolean attribute written as `"true"` or `"false"` so the
inversion cannot fail silently.

Attributes whose `"false"` is meaningful `aria-*`, `contenteditable` should
Attributes whose `"false"` is meaningful (`aria-*`, `contenteditable`) should
be declared as **string** props.

See it live: [Boolean props demo ↗](https://demo.webcomponent.io/examples/boolean-props/)
40 changes: 20 additions & 20 deletions docs/src/content/docs/guides/cem-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ slug: cem-plugin
import { Aside } from '@astrojs/starlight/components'

A CEM (`custom-elements.json`) is a standard description of the elements a
package defines their tags, attributes, properties, events and slots so
package defines (their tags, attributes, properties, events and slots) so
tooling can read your components without executing them. Read more at
[custom-elements-manifest.open-wc.org](https://custom-elements-manifest.open-wc.org/)
for the analyzer and its plugin API, or the [schema and
Expand All @@ -20,7 +20,7 @@ We provide a CEM Analyzer Plugin `web-component-base/cem-plugin` which allows us
set up: a starter component, this plugin configured in
`custom-elements-manifest.config.mjs`, an `analyze` script, and the
`customElements` field set in `package.json`. The rest of this guide is the
manual path use it to add the manifest to an existing project, or to
manual path: use it to add the manifest to an existing project, or to
understand what the scaffold configured.
</Aside>

Expand Down Expand Up @@ -53,10 +53,10 @@ npx cem analyze
Run it **from the directory holding your config and a `package.json`**. Two
things make it appear to do nothing, neither of which prints an error:

- **No config found** it falls back to a default glob of
- **No config found**: it falls back to a default glob of
`**/*.{js,ts,tsx}`, which does _not_ exclude `node_modules`. In a real
project that is tens of thousands of files through the TypeScript parser.
- **No `package.json` in the directory** it hangs outright.
- **No `package.json` in the directory**: it hangs outright.

With globs scoped to your source it should finish in well under a second.

Expand Down Expand Up @@ -92,10 +92,10 @@ customElements.define('cozy-button', CozyButton)

Two details worth knowing:

- **Types come from the default literal** `true`/`false` → `boolean`, numeric → `number`, object/array → `object`, everything else → `string`.
- **Types come from the default literal**: `true`/`false` → `boolean`, numeric → `number`, object/array → `object`, everything else → `string`.
- **Attribute names come from wcb's own `getKebabCase`**, the same function `observedAttributes` uses, so manifest names can't drift from what the component actually observes.

The defaults may be written inline or hoisted into a module-level `const` — the latter is required by the [typed props](/prop-access/#typed-props-in-typescript) pattern, and the plugin resolves it either way.
The defaults may be written inline or hoisted into a module-level `const`. The latter is required by the [typed props](/prop-access/#typed-props-in-typescript) pattern, and the plugin resolves it either way.

## Storybook

Expand All @@ -113,7 +113,7 @@ setCustomElementsManifest(manifest)
export default { tags: ['autodocs'] }
```

Bind a story to the tag name and Storybook infers the rest a text field for `variant`, a toggle for `disabled`, a number input for `maxCount`:
Bind a story to the tag name and Storybook infers the rest, giving a text field for `variant`, a toggle for `disabled`, a number input for `maxCount`:

```js
// cozy-button.stories.js
Expand All @@ -133,19 +133,19 @@ export const Default = { args: { variant: 'primary', disabled: false } }

<Aside type="tip">
Regenerate the manifest before starting Storybook (`cem analyze && storybook
dev`) `custom-elements.json` is a build artifact, so it is usually
dev`). `custom-elements.json` is a build artifact, so it is usually
gitignored and rebuilt on demand.
</Aside>

This repo runs exactly this setup against the demo components in `storybook/` — see it there for a working reference.
This repo runs exactly this setup against the demo components in `storybook/`. See it there for a working reference.

## Code editors

Once `custom-elements.json` exists, editors can offer tag-name and attribute autocomplete for your components driven by the same `static props` the plugin reads, so the hints can't drift from the code.
Once `custom-elements.json` exists, editors can offer tag-name and attribute autocomplete for your components, driven by the same `static props` the plugin reads, so the hints can't drift from the code.

<Aside type="caution">
VS Code does **not** read `custom-elements.json` natively. Nothing happens
just because the file exists — you need one of the two routes below.
just because the file exists. You need one of the two routes below.
</Aside>

First, point tooling at the manifest from your `package.json`. This is the field every option here uses to discover it:
Expand All @@ -156,7 +156,7 @@ First, point tooling at the manifest from your `package.json`. This is the field
}
```

### Route 1 native VS Code, no extension
### Route 1: native VS Code, no extension

VS Code's built-in HTML language service reads its own [custom data](https://github.com/microsoft/vscode-custom-data) format. A second analyzer plugin converts the manifest into it, so both files come out of one `cem analyze` run:

Expand Down Expand Up @@ -187,25 +187,25 @@ export default {
`html.customData` paths resolve from the **workspace root**, not from the
settings file. If you run `cem analyze` in a subfolder, either point at
`./that-folder/vscode.html-custom-data.json` or give the generator its own
`outdir``generateCustomData({ outdir: '..' })` so the file lands where
`outdir`, `generateCustomData({ outdir: '..' })`, so the file lands where
the setting expects it.
</Aside>

Restart VS Code and `<cozy-` completes in HTML files, with `variant` / `disabled` / `max-count` offered as attributes and their types and defaults on hover.

**The catch:** `html.customData` only applies to `.html` files. wcb components author their markup in `html` tagged templates inside `.js` / `.ts`, and those do not go through the HTML language service. This route helps whoever writes plain HTML pages against your components — it will not light up inside your own templates.
**The catch:** `html.customData` only applies to `.html` files. wcb components author their markup in `html` tagged templates inside `.js` / `.ts`, and those do not go through the HTML language service. This route helps whoever writes plain HTML pages against your components. It will not light up inside your own templates.

### Route 2 a language server extension, for tagged templates
### Route 2: a language server extension, for tagged templates

To get the same completions **inside** tagged templates, you need an extension that understands them. The most current option is the [Custom Elements Manifest Language Server](https://marketplace.visualstudio.com/items?itemName=pwrs.cem-language-server-vscode) (`pwrs.cem-language-server-vscode`). It autocompletes tag names and attributes inside template literals in both JS and TS, adds hover documentation for attributes and defaults, and discovers the manifest through the `customElements` field above no `.vscode/settings.json` needed.
To get the same completions **inside** tagged templates, you need an extension that understands them. The most current option is the [Custom Elements Manifest Language Server](https://marketplace.visualstudio.com/items?itemName=pwrs.cem-language-server-vscode) (`pwrs.cem-language-server-vscode`). It autocompletes tag names and attributes inside template literals in both JS and TS, adds hover documentation for attributes and defaults, and discovers the manifest through the `customElements` field above, no `.vscode/settings.json` needed.

Two alternatives, both worth knowing the state of:

- [`wc-toolkit/wc-language-server`](https://wc-toolkit.com/integrations/web-components-language-server/) VS Code and JetBrains, also manifest-driven. Self-described as **alpha and experimental**.
- `Matsuuu.custom-elements-language-server-project` the one you'll find most often in older write-ups. It is alpha, and **its repository was archived in January 2026**, so prefer one of the two above.
- [`wc-toolkit/wc-language-server`](https://wc-toolkit.com/integrations/web-components-language-server/): VS Code and JetBrains, also manifest-driven. Self-described as **alpha and experimental**.
- `Matsuuu.custom-elements-language-server-project`: the one you'll find most often in older write-ups. It is alpha, and **its repository was archived in January 2026**, so prefer one of the two above.

<Aside type="note">
This corner of the ecosystem moves quickly and every option is pre-1.0. Route
1 is the conservative choice it is plain VS Code configuration with no
extension to go stale at the cost of not covering tagged templates.
1 is the conservative choice (it is plain VS Code configuration with no
extension to go stale) at the cost of not covering tagged templates.
</Aside>
10 changes: 5 additions & 5 deletions docs/src/content/docs/guides/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ Numbers below are **measured** from the same minimal counter component (one reac
| `@elenajs/core` | 1.0.0 | 9.1 kB | 3.7 kB | 3.3 kB |
| `lit` | 3.3.3 | 15.3 kB | 5.9 kB | 5.3 kB |
| `@microsoft/fast-element` | 3.0.1 | 44.9 kB | 13.7 kB | 12.2 kB |
| vanilla `HTMLElement` | | | | ~0.2 kB |
| vanilla `HTMLElement` | - | - | - | ~0.2 kB |

For scale: even after all of the v5.2–v6.1 work, the WCB counter is **~21% smaller than Elena, ~51% smaller than Lit, and ~79% smaller than FAST**.

## Feature comparison

What each library gives you beyond extending directly from `HTMLElement` the boilerplate you no longer write by hand:
What each library gives you beyond extending directly from `HTMLElement`, the boilerplate you no longer write by hand:

| Capability | WCB 6.1 | Lit 3.3 | Elena 1.0 | FAST 3.0 |
| -------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------- | --------------------------------------- | ------------------------------------------------ |
Expand All @@ -42,11 +42,11 @@ What each library gives you beyond extending directly from `HTMLElement` — the
| Backing / ecosystem | solo maintainer, small surface | Google, huge ecosystem | new (2026), design-system focus | Microsoft, powers Fluent UI |

:::note[Why 11ty WebC isn't here]
WebC is a compile-time tool: it resolves components during an Eleventy build and ships plain HTML with no client runtime. Every row above is about what a library does _in the browser at runtime_, so a side-by-side comparison would be measuring two different things. If your components are static at build time, WebC solves a different problem and solves it well.
WebC is a compile-time tool: it resolves components during an Eleventy build and ships plain HTML with no client runtime. Every row above is about what a library does _in the browser at runtime_, so a side-by-side comparison would be measuring two different things. If your components are static at build time, WebC solves a different problem, and solves it well.
:::

For what these numbers and capabilities add up to and when they don't see [Why would anyone use WCB?](/why/).
For what these numbers and capabilities add up to (and when they don't) see [Why would anyone use WCB?](/why/).

---

_WCB re-measured 2026-07-20 at v6.1.0; the other libraries measured 2026-07-19, with esbuild, Node zlib (gzip −9, brotli q11), at the pinned versions above. Methodology: identical counter component per library, bundled per library, compressed. Re-run them yourself — the benchmark is trivially reproducible with the versions pinned above._
_WCB re-measured 2026-07-20 at v6.1.0; the other libraries measured 2026-07-19, with esbuild, Node zlib (gzip −9, brotli q11), at the pinned versions above. Methodology: identical counter component per library, bundled per library, compressed. Re-run them yourself. The benchmark is trivially reproducible with the versions pinned above._
2 changes: 1 addition & 1 deletion docs/src/content/docs/guides/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ slug: examples
## Live demo gallery

Every example below runs as a standalone page at
[demo.webcomponent.io ↗](https://demo.webcomponent.io/) a live gallery with
[demo.webcomponent.io ↗](https://demo.webcomponent.io/), a live gallery with
the source alongside each demo.

| Demo | Shows |
Expand Down
Loading
Loading