Installation

Usage Guides

Reference

Commands

Language Server

MCP Server

Documenting Components

TL;DR: Use inline HTML comments for slots and parts, and CSS-source comments for custom properties — they stay co-located with the markup and styles they describe. JSDoc tags (@slot, @csspart, @cssprop, @fires, @attr) are an alternative, and remain the preferred choice for @fires and @attr. See examples below.

Use JSDoc comments to document your custom elements for the manifest . The manifest powers LSP features like autocomplete and validation, enables AI assistants to understand your components, and drives the dev server’s interactive controls .

JSDoc Tags

Use these tags in your element class and member JSDoc comments. See the generate command reference for the complete list.

Basic Example

import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

/**
 * Displays a personalized greeting message
 *
 * @summary A simple greeting component
 * @slot - Default slot for custom content
 * @csspart greeting - The greeting text container
 * @cssprop --greeting-color - Text color (default: currentColor)
 */
@customElement('hello-world')
export class HelloWorld extends LitElement {
  /**
   * The name to greet
   */
  @property() name = 'World';

  render() {
    return html`
      <div id="greeting" part="greeting">
        Hello, ${this.name}!
        <slot></slot>
      </div>
    `;
  }
}

Specifying Tag Names

When the tag name can’t be detected automatically, use @customElement, @element, or @tagName:

/**
 * A vanilla custom element
 *
 * @customElement vanilla-element
 */
class MyElement extends HTMLElement {
  static is = 'vanilla-element';

  static {
    customElements.define(this.is, this);
  }
}

All three tags are aliases and work identically:

  • @customElement vanilla-element (recommended)
  • @element vanilla-element
  • @tagName vanilla-element

The generator automatically detects tag names from the @customElement decorator and customElements.define('tag-name', Class) calls with static strings. However, when using dynamic patterns like customElements.define(this.is, this) where the tag name is stored in a variable, you must use one of the JSDoc tags above to specify the tag name explicitly.

Documenting Slots and Parts

cem automatically detects <slot> elements and part attributes in your template. You can add descriptions using JSDoc tags or inline HTML comments.

Prefer inline HTML comments over JSDoc @slot and @csspart tags for documenting slots and parts. HTML comments stay co-located with the markup they describe, making them easier to maintain.

Plain Comments

A plain HTML comment immediately before an element is the simplest way to add a description. This works for both slots and parts:

<!-- Button label content. SHOULD contain text. -->
<slot></slot>

<!-- The native button element -->
<button part="button">

Single-Key YAML

When you need a short summary label (shown in tooling previews) rather than a full description, use a single-key YAML comment:

<!-- summary: Button label content -->
<slot></slot>

<!-- summary: The native button element -->
<button part="button">

YAML Metadata

When you need separate summary and description fields, or want to mark something as deprecated, use multi-key YAML syntax inside the comment:

<!--
  summary: The main slot for content
  description: |
    This slot displays user-provided content.
    Supports multiline **markdown**.
  deprecated: true
-->
<slot></slot>

Combined Slot and Part

When the same element has both a slot and part attribute and you want separate documentation for each, use nested slot: and part: keys.

A scalar string is shorthand for description, so you can mix scalar and object forms when one side only needs a description and the other needs full metadata:

<!-- slot:
       summary: The `info` slot
     part:
       summary: The `info-part` part -->
<slot name="info" part="info-part"></slot>

<!-- slot: The info slot
     part:
       summary: Short label
       description: Longer description of the part -->
<slot name="info" part="info-part"></slot>

<!-- part: The overlay container
     slot:
       summary: Overlay
       description: Content shown in the overlay -->
<slot name="overlay" part="overlay"></slot>
When including inline markdown `code` in lit-html templates, escape the backticks in the comment.

Documenting CSS Custom Properties

Prefer documenting CSS custom properties in your CSS sources rather than with JSDoc @cssprop tags on the class. CSS comments keep the documentation co-located with the property definitions.

Document CSS variables using JSDoc-style comments in your CSS:

:host {
  /**
   * A property defined on the host
   * @summary The host's custom property
   */
  --host-property: red;

  color:
    /**
     * Custom color for use in this element
     * @summary color
     * @deprecated Use the `color` property instead
     */
    var(--custom-color);

  border:
    1px solid
    /** Border color of the element */
    var(--border-color);
}

Position comments correctly when both LHS and RHS contain CSS custom properties:

/** Comment for --a */
color: var(--a);

/** Comment for --b */
--b: blue;

/** Comment for --c */
--c:
  /** Comment for --d */
  var(--d);

Design Token Integration

Use the --design-tokens flag to integrate DTCG-format design tokens:

cem generate --design-tokens npm:@my-ds/tokens/tokens.json --design-tokens-prefix my-ds

npm: and jsr: specifiers resolve from node_modules first. If the package isn’t installed locally, cem fetches it from esm.sh automatically.

The prefix should not include leading dashes — use my-ds, not --my-ds.

When both user comments and design token descriptions exist for the same property, both are included (user description first, then design token description).

Documenting Demos

JSDoc @demo Tag

Link to demos directly from your element class:

/**
 * @demo https://example.com/my-element-plain/
 * @demo https://example.com/my-element-fancy/ - A fancier demo
 */
@customElement('my-element')
class MyElement extends LitElement { }

Automatic Demo Discovery

Configure automatic discovery in .config/cem.yaml:

sourceControlRootUrl: "https://github.com/your/repo/tree/main/"
generate:
  demoDiscovery:
    fileGlob: "src/**/demos/*.html"
    urlPattern: "/src/:tag/demos/:demo.html"
    urlTemplate: "https://example.com/elements/{{.tag | alias}}/{{.demo | slug}}/"

Template functions:

  • alias - Apply element alias mapping
  • slug - Convert to URL-friendly format
  • lower - Convert to lowercase
  • upper - Convert to uppercase

See Working with Demos for organization strategies.

Code Examples

Use the @example tag for code examples:

/**
 * @example Basic usage
 *          ```html
 *          <my-element></my-element>
 *          ```
 */
@customElement('my-element')
class MyElement extends LitElement { }

With explicit caption:

/**
 * @example
 * <caption>Advanced usage with properties</caption>
 * ```html
 * <my-element color="primary" size="large"></my-element>
 * ```
 */

Multiple examples:

/**
 * A flexible element
 * @example Simple case
 *          ```html
 *          <my-element></my-element>
 *          ```
 * @example With attributes
 *          ```html
 *          <my-element foo="bar"></my-element>
 *          ```
 */

Examples with captions are wrapped in <figure>/<figcaption> elements in the generated manifest.

Documenting Methods

Document public methods using @param and @returns tags:

/**
 * Updates the element's theme
 *
 * @summary Apply a new theme
 * @param {string} themeName - Name of the theme to apply
 * @param {boolean} [persist=false] - Whether to save theme preference
 * @returns {boolean} True if theme was applied successfully
 * @example
 * ```typescript
 * element.setTheme('dark', true);
 * ```
 */
setTheme(themeName: string, persist = false): boolean {
  // implementation
}

Parameter syntax:

  • Required: @param {type} name - description
  • Optional: @param {type} [name] - description
  • With default: @param {type} [name=default] - description

Monorepo Setup

For npm or yarn workspaces, create a .config/cem.yaml file in each package:

Root package.json:

{
  "scripts": {
    "generate": "npm run generate --workspaces"
  },
  "workspaces": ["./core", "./elements"]
}

core/.config/cem.yaml:

generate:
  files:
    - './**/*.ts'

core/package.json:

{
  "scripts": {
    "generate": "cem generate"
  }
}

Repeat for each workspace package.

See Also