https://github.com/morganney/web-component-best-practices
Some best practices regarding web component architecture, development, building and publishing.
https://github.com/morganney/web-component-best-practices
css html javascript web-components
Last synced: 22 days ago
JSON representation
Some best practices regarding web component architecture, development, building and publishing.
- Host: GitHub
- URL: https://github.com/morganney/web-component-best-practices
- Owner: morganney
- License: mit
- Created: 2022-09-29T20:31:34.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2026-05-26T15:28:30.000Z (23 days ago)
- Last Synced: 2026-05-26T17:21:14.601Z (23 days ago)
- Topics: css, html, javascript, web-components
- Language: HTML
- Homepage: https://morganney.github.io/web-component-best-practices/
- Size: 182 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Web Component Best Practices
A practical reference for architecting, developing, and publishing modern HTML custom elements with minimal tooling.
## Constraints (self-imposed)
- Use as little tooling as possible.
- ES [modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) only.
- Consumable directly from a [CDN](https://unpkg.com/).
- Consumable as an npm package in bundlers like Vite, Rollup, and Webpack.
- Keep each technology in a separate file (HTML, CSS, JS/TS).
## Architecture
The core pattern is strict separation of concerns:
- **HTML** in `template.html`
- **CSS** in `styles.css`
- **Component class/runtime** in `element.js`
- **Registration side effect** in `defined.js`
Current example layout:
```text
example/
index.html
src/
template.html
styles.css
element.js
defined.js
```
### `styles.css`
- Standard CSS for the component [ShadowRoot](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot).
- Loaded by `element.js` and injected into the template as a `` element.
### `template.html`
- One root [`<template>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).
- Contains component markup and named/default [`<slot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) regions.
- Fetched by `element.js` and cloned into shadow DOM.
### `element.js`
- Defines the custom element class (`extends HTMLElement`).
- Handles lifecycle behavior and shadow-root setup.
- Uses [top-level `await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#top_level_await) so dependent modules wait for template/styles setup.
- Exposes `register(name?)` for explicit, side-effect-free registration.
### `defined.js`
- Encapsulates the side effect of registration (`customElements.define(...)`).
- Supports dynamic element names through query params (for example `?name=my-element`).
- Uses `whenDefined(...)` and a duplicate-define guard for safer repeated imports.
## Example behavior
`example/index.html` demonstrates four registration patterns with the same underlying component class:
1. **Explicit registration (no side effect)** via `element.js` + `register(...)`
2. **Default side-effect registration** via `defined.js`
3. **Local dynamic name** via `defined.js?name=dynamic-name`
4. **CDN dynamic name** via `defined.js?name=cdn-dynamic-name`
## Declarative Shadow DOM (DSD) approach
This repo also includes a parser-time DSD variant in `example/dsd.html`.
- `example/dsd.html` is the DSD page template.
- `example/src/dsd/template.js` provides reusable HTML template strings for the component cards.
- `vite.config.js` injects those templates into `<!-- inject:cards -->` at build time.
This gives you true parser-time shadow roots in the generated HTML while keeping the card markup DRY in source.
### Why `example/src/dsd/register.js` exists
Parser-time DSD creates shadow roots from HTML, but custom elements still need to be defined to upgrade and run lifecycle code.
`example/src/dsd/register.js` is a focused bootstrap module that registers all demo tag-name variants used on the DSD page:
1. explicit registration for the default tag name via `element.js` + `register(...)`
2. explicit registration for `dynamic-name` via `register('dynamic-name')`
3. explicit registration for `no-side-effects` via `register('no-side-effects')`
4. CDN dynamic-name registration via `dsd/defined.js?name=cdn-dynamic-name`
Without this bootstrap file, DSD markup would still parse, but the custom elements on that page would not upgrade.
## Tradeoffs
There is a hard constraint triangle for this problem space. Today, you can reliably pick two of these three goals at once: true parser-time DSD, DRY shared markup, and no build/no server composition.
| Keep | Implementation | Tradeoff |
| -------------- | ------------------------------------- | ------------------------------ |
| DSD + no build | duplicate markup in each HTML file | not DRY (drift risk) |
| DRY + no build | runtime JS composition/fetch | not true parser-time DSD |
| DSD + DRY | build step or server-side composition | cannot stay no-build/no-server |
For this repo, the DSD path chooses DSD + DRY via build-time composition, while the runtime path keeps separate source files with no required build for local static serving.
## Related example (`youtube-vid`)
For a production-oriented implementation of these patterns, see:
- https://github.com/morganney/youtube-vid
That project demonstrates the same architectural goals with a different packaging decision:
- It uses [Vite asset bundling](https://vitejs.dev/guide/assets#importing-asset-as-string) to include HTML/CSS and reduce runtime requests.
- It also includes an example [CLI copy script](https://github.com/morganney/youtube-vid/blob/main/src/cli.ts) for workflows that prefer shipping static assets separately.
Historical context:
- Original non-bundled implementation: https://github.com/morganney/youtube-vid/tree/3d7b8ac817170cff8bba036c1a938042a0e0b76f
- Example consumer usage in a Next.js app: https://github.com/morganney/morgan.neys.info/commit/9771143e1c7c7e6f82baf0a11948cba5a1304c3f#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519R12