Skip to content

Publish a Custom Elements Manifest

New in v0.24.0

Ship a custom-elements.json that documents your components, written from the same source reading the DevTools panel uses. Skip this page if you already generate one with the analyzer and are happy with it; Manifest generators compared sets the tools side by side.

A Custom Elements Manifest is a JSON file that lists each element’s tag, properties, attributes, events, slots, parts and CSS properties. Editors, Storybook and documentation sites read it, and a package points at it with the customElements field in its package.json.

Set manifest: true. vite build then emits custom-elements.json beside the bundle, and the dev server serves it at /custom-elements.json.

vite.config.ts
import {defineConfig} from 'vite';
import {litPlugin} from '@oddsquad/lit-devtools/vite';
export default defineConfig({
plugins: [litPlugin({manifest: true})],
});

The build reads every module of the bundle, so its manifest covers each component the bundle contains. Each module path names the built file that holds the component, such as dist/my-lib.js, because that is what a consumer imports. The dev server reads every script under src when it starts and follows each change on disk, so its manifest lists your components before any page loads them. Set scan to the folders your components live in, or to false to list only what pages load. It links the manifest in each page’s head, so the browser extension shows the docs too.

Document a component with JSDoc on the class and its fields:

src/x-button.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
/**
* A button that submits the form it sits in.
*
* @summary The form's submit button.
* @fires submit-start - Before the form is sent.
* @slot - The label.
* @csspart base - The native button.
* @cssprop [--x-button-gap=4px] - Space between icon and label.
*/
@customElement('x-button')
export class XButton extends LitElement {
/** How much room the button takes. */
@property({reflect: true}) size: 'small' | 'large' = 'small';
render() {
return html`<button part="base"><slot></slot></button>`;
}
}

The class comment gives the description and each tagged entry. A field’s comment gives its description, its type annotation the type, and its initializer the default. @property() names the attribute it maps to, and reflect: true marks it reflects. @state() and private members stay out. The components guide lists every tag the reader understands.

Public methods are listed too, with their parameters and return type. Types come from the annotations, or from @param {Type} name - description and @returns {Type} description in plain JavaScript. Lifecycle callbacks such as render(), updated() and connectedCallback() are how Lit calls your class, so they stay out.

A class lists the members it inherits from your own base classes and mixins, each marked inheritedFrom, so tools need no inheritance walk. Types are the text you wrote; nothing is inferred from the checker.

Add write to keep the file in your project as well. The dev server rewrites it as you edit, and vite build rewrites it from every module of the bundle.

vite.config.ts
import {defineConfig} from 'vite';
import {litPlugin} from '@oddsquad/lit-devtools/vite';
export default defineConfig({
plugins: [litPlugin({manifest: {write: true}})],
});

When your package.json has no customElements field yet, the plugin sets it to the file it wrote. A field you already set is left alone.

lit-devtools manifest reads every script under src, not only the ones a build reached, and writes custom-elements.json. Run it before you publish:

package.json
{
"scripts": {
"prepublishOnly": "lit-devtools manifest"
}
}

It resolves imports through your Vite config, so aliases work; without one, through tsconfig.json paths and package exports. The CLI reference lists its flags.