Skip to content

Distributing and Browser Support

A web component is just a JavaScript module. Publish it the same way you would any ES module package. A minimal package.json:

{
"name": "my-counter",
"version": "1.0.0",
"type": "module",
"main": "./dist/my-counter.js",
"module": "./dist/my-counter.js",
"exports": {
".": "./dist/my-counter.js"
},
"files": ["dist"],
"keywords": ["web-components", "custom-element"]
}

Consumers install it with npm install my-counter and import it once (a side-effect import is enough to register the tag):

import 'my-counter';
// <my-counter> is now usable anywhere in the document

The Custom Elements Manifest (custom-elements.json) is a machine-readable JSON file that documents every custom element in a package — its tag name, properties, attributes, events, and CSS custom properties. Tools like Storybook, IDEs, and design-system dashboards consume it for auto-complete and documentation.

Generate one with the @custom-elements-manifest/analyzer CLI:

Terminal window
npm install --save-dev @custom-elements-manifest/analyzer
npx cem analyze --globs "src/**/*.js"

A fragment of what it produces:

{
"schemaVersion": "1.0.0",
"modules": [{
"kind": "javascript-module",
"path": "src/my-counter.js",
"declarations": [{
"kind": "class",
"name": "MyCounter",
"customElement": true,
"tagName": "my-counter",
"attributes": [
{ "name": "label", "type": { "text": "string" } }
],
"members": [
{ "kind": "field", "name": "count", "type": { "text": "number" } }
]
}]
}]
}

Follow semantic versioning. Breaking changes in a custom element are:

  • Removing or renaming an attribute, property, or event.
  • Changing the element’s tag name.
  • Changing the shape of a custom event’s detail object.

Non-breaking changes include adding new optional attributes, new CSS custom properties, or new public methods.

As of 2025, Custom Elements v1, Shadow DOM v1, and HTML Templates are supported in every major browser — Chrome, Firefox, Safari, and Edge (all current and most previous versions). The “Baseline Widely Available” badge applies to all three pillars.

The one exception: customized built-ins (elements that extend a built-in like HTMLButtonElement with is="...") are not supported in Safari. Prefer autonomous custom elements (extends HTMLElement) for maximum portability.

Lit itself supports Chrome 104+, Firefox 121+, Safari 16.4+. For production apps targeting older browsers, the @webcomponents/polyfills package fills gaps.

The demo below shows a simple distributable badge element that illustrates the “published package feel” — exactly what a consumer would see after installing your component from npm.

What is the purpose of the Custom Elements Manifest file?
Which type of custom element is NOT supported in Safari?
What does importing 'my-counter' (a side-effect import) do?