Web Components in Easy Mode https://webcomponent.io
Find a file
Ayo 2da6cd5afc
Some checks are pending
Tests / Unit (push) Waiting to run
Tests / E2E (push) Waiting to run
feat: new cem analyzer plugin (cem-plugin)
2026-07-19 21:06:22 +02:00
.claude chore: new demo workspace & e2e tests 2026-07-05 15:40:53 +02:00
.github/workflows ci: drop gh packages publish 2026-07-05 19:33:16 +02:00
.husky chore: nightly full e2e and size limit tests 2026-07-05 16:33:10 +02:00
.vscode feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
assets chore: add todo-app example on readme 2023-12-27 22:11:59 +01:00
demo feat: typed props 2026-07-19 18:27:52 +02:00
docs feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
scripts chore: update release script 2026-07-05 12:25:36 +02:00
src feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
storybook feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
test feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
vendors/htm chore: publish dist from root dir (#32) 2023-12-18 23:12:18 +01:00
.gitignore feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
.npmignore chore: new demo workspace & e2e tests 2026-07-05 15:40:53 +02:00
.nvmrc chore: add .nvmrc 2026-07-05 12:17:31 +02:00
.prettierignore chore: set up prettier ignore 2024-12-27 11:10:32 +01:00
AGENTS.md feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
eslint.config.mjs feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
LICENSE chore: add LICENSE file 2023-11-10 21:56:12 +01:00
netlify.toml feat: replace mcfly site with starlight docs 2025-03-26 21:10:27 +01:00
package.json feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
pnpm-lock.yaml feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
pnpm-workspace.yaml feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
prettier.config.mjs chore: use .mjs config files 2024-12-19 23:16:57 +01:00
README.md feat: new cem analyzer plugin (cem-plugin) 2026-07-19 21:06:22 +02:00
size-change-log.md feat: buffer attribute changes until end of onInit 2026-07-05 11:54:44 +02:00
tsconfig.json chore: add rootDir in tsconfig 2026-05-08 14:44:20 +02:00
vitest.config.mjs chore: new demo workspace & e2e tests 2026-07-05 15:40:53 +02:00
vitest.e2e.config.mjs chore: add github test actions 2026-07-05 16:10:35 +02:00

Web Component Base

Note

Correctness fixes shipped (v5.0.0) — We now have better compliance with the custom elements specs, better property type handling and clearer distinction between attributes and property within onChanges() hook (breaking change). See the change log for more details. Next up: faster templates!

Package information: NPM version Package information: NPM license Package information: NPM downloads Bundle Size

🤷‍♂️ zero-dependency, 🤏 tiny JS base class for creating reactive custom elements easily

This is the base class used for web components in Ayo's projects, primarily cozy-games, mcfly, his personal site, his blog, and others.

Read more about it on the docs or view a demo on CodePen.

counter example code snippet

When you extend the WebComponent class for your component, you only have to define the template and properties. Any change in any property value will automatically cause just the component UI to render.

The result is a reactive UI on property changes.

TypeScript: typed props

this.props is untyped ({ [name: string]: any }) by default. Pass the shape of your defaults as a type argument to get compile-time types on declared props:

const props = { variant: 'primary', disabled: false }

class CozyButton extends WebComponent<typeof props> {
  static props = props

  get template() {
    this.props.variant // string
    this.props.disabled // boolean
    this.props.disabled = 'yes' // ❌ compile error
    return html`<button class=${this.props.variant}></button>`
  }
}

The runtime is unchanged — this is types-only, and omitting the type argument keeps the previous behavior. See the prop access guide for details.

Storybook autodocs & controls

Storybook infers autodocs and controls from a Custom Elements Manifest, but the stock analyzer reads static props as one opaque object and emits no attributes. web-component-base/cem-plugin teaches it the convention — dev-time only, so the core stays zero-dependency:

// custom-elements-manifest.config.mjs
import { wcbStaticProps } from 'web-component-base/cem-plugin'

export default {
  globs: ['src/**/*.js'],
  outdir: '.',
  plugins: [wcbStaticProps()],
}

npx cem analyze then emits a typed attribute per prop — variant (string), disabled (boolean), maxCountmax-count (number) — named with wcb's own getKebabCase so they match observedAttributes, with wcb internals stripped. Point Storybook at the result:

// .storybook/preview.js
import { setCustomElementsManifest } from '@storybook/web-components-vite'
import manifest from '../custom-elements.json'

setCustomElementsManifest(manifest)
export default { tags: ['autodocs'] }

A story only needs component: 'cozy-button' — no per-story argTypes. See the full recipe, or the working setup in storybook/.

Want to get in touch?

There are many ways to get in touch:

  1. Open a GitHub issue or discussion
  2. Submit a ticket via SourceHut todo
  3. Email me: hi@ayo.run

Inspirations and thanks

  1. htm - I use it for the html function for tagged templates, and take a lot of inspiration in building the rendering implementation. It is highly likely that I will go for what Preact is doing... but we'll see.
  2. fast - When I found that Microsoft has their own base class I thought it was super cool!
  3. lit - lit-html continues to amaze me and I worked to make wcb generic so I (and others) can continue to use it

Size change log

See size-change-log.md for a running record of how each correctness/feature change affects the WebComponent base class bundle size, with the reason and benefit of each.


Just keep building.
A project by Ayo