Skip to content
custom-elements logo

Custom Elements Skill

custom-elements

Define and use custom HTML elements. Use when creating new components, defining custom tags, or using project-specific elements beyond standard HTML5.

SKILL.md

Full skill instructions

Custom Elements Skill

This skill provides guidance for defining and using custom HTML elements in this project.

Two Definition Systems

SystemFilePurpose
HTML Validation.claude/​schemas/​elements.jsonValidates custom elements in HTML (html-validate)
Custom Elements Manifestcustom-elements.jsonDocuments components for IDEs, Storybook, docs

Both are recommended - elements.json for build-time HTML validation, CEM for runtime tooling.

See MANIFEST.md for Custom Elements Manifest generation.

Using Existing Elements

Check .claude/​schemas/​elements.json for defined custom elements:

ElementTypePurpose
product-cardBlockProduct display with sku, price attributes
icon-elementVoidSelf-closing icon with required name attribute
user-avatarVoidAvatar with required src, alt and optional size
status-badgeInlineStatus indicator with type (success/​warning/​error/​info)
data-tableBlockData table with source, sortable attributes
nav-menuBlockNavigation with orientation (horizontal/​vertical)

Usage Examples

<!-- Product card with attributes -->
<product-card sku="ABC123" price="29.99">
  <h2>Product Name</​h2>
  <p>Product description here.</​p>
</​product-card>

<!-- Void element (self-closing) -->
<icon-element name="star"/>
<user-avatar src="/​avatar.jpg" alt="John Doe" size="medium"/>

<!-- Inline element -->
<p>Status: <status-badge type="success">Active</​status-badge></​p>

<!-- Block elements -->
<nav-menu orientation="horizontal">
  <ul>
    <li><a href="/">Home</​a></​li>
  </​ul>
</​nav-menu>

<data-table source="/​api/​users" sortable="">
  <!-- Table content -->
</​data-table>

Ad-hoc Custom Elements

For one-off custom elements not worth defining in .claude/​schemas/​elements.json, use the x-* prefix:

<x-highlight>Important text</​x-highlight>
<x-tooltip data-text="Help text">Hover me</​x-tooltip>
<x-badge>New</​x-badge>

The x-* pattern is excluded from validation by default.

Defining New Elements

Using Slash Command

/​add-element my-widget

Manual Definition

Add to .claude/​schemas/​elements.json:

{
  "my-element": {
    "flow": true,
    "phrasing": false,
    "permittedContent": ["@flow"],
    "attributes": {
      "required-attr": { "required": true },
      "optional-attr": { "required": false }
    }
  }
}

Element Schema Reference

Content Model

PropertyValuesDescription
flowbooleanCan appear where flow content is expected
phrasingbooleanCan appear where phrasing content is expected
voidbooleanSelf-closing element (no content)
permittedContentarrayWhat content is allowed inside

Permitted Content Values

  • @flow - Flow content (most elements)
  • @phrasing - Phrasing content (inline elements)
  • @interactive - Interactive elements
  • ["p", "div"] - Specific elements only

Attribute Definitions

{
  "attributes": {
    "name": {
      "required": true           // Must be present
    },
    "type": {
      "required": false,
      "enum": ["a", "b", "c"]    // Restricted values
    },
    "enabled": {
      "boolean": true            // Boolean attribute
    }
  }
}

Complete Examples

Block Element

{
  "card-component": {
    "flow": true,
    "phrasing": false,
    "permittedContent": ["@flow"],
    "attributes": {
      "variant": {
        "required": false,
        "enum": ["default", "outlined", "elevated"]
      },
      "clickable": {
        "boolean": true
      }
    }
  }
}

Usage:

<card-component variant="elevated" clickable="">
  <h2>Card Title</​h2>
  <p>Card content</​p>
</​card-component>

Void Element

{
  "loading-spinner": {
    "void": true,
    "flow": true,
    "phrasing": true,
    "attributes": {
      "size": {
        "enum": ["small", "medium", "large"]
      }
    }
  }
}

Usage:

<loading-spinner size="medium"/>

Inline Element

{
  "price-tag": {
    "flow": true,
    "phrasing": true,
    "permittedContent": ["@phrasing"],
    "attributes": {
      "currency": {
        "required": false,
        "enum": ["USD", "EUR", "GBP"]
      }
    }
  }
}

Usage:

<p>Price: <price-tag currency="USD">29.99</​price-tag></​p>

Naming Conventions

  • Use lowercase with hyphens: my-element
  • Names must contain a hyphen (web component spec)
  • Be descriptive: product-card not pc
  • Use consistent prefixes for related elements: form-input, form-select, form-button

CSS-Only vs Full Web Components

Custom elements can be used in two distinct ways. Choose based on your needs.

CSS-Only Custom Elements

Use the element as a semantic styling hook without JavaScript:

<product-card>
  <img src="product.jpg" alt="Widget Pro" />
  <h3>Widget Pro</​h3>
  <p>The best widget money can buy.</​p>
</​product-card>
product-card {
  display: block;  /​* Required: browsers default to inline */
  padding: var(--spacing-lg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

Characteristics:

  • No JavaScript required
  • No customElements.define() call
  • Matches :not(:defined) pseudo-class
  • Requires explicit display: block (handled by shared reset)
  • Content is in light DOM (no Shadow DOM)
  • Styles work immediately, no FOUC

Best for:

  • Semantic styling hooks (replacing .card classes)
  • Progressive enhancement base
  • Static content components
  • Simple layout containers

Full Web Components

Register the element with JavaScript for encapsulated behavior:

class ProductCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; }
        /​* Encapsulated styles */
      </​style>
      <slot></​slot>
    `;
  }
}

customElements.define('product-card', ProductCard);

Characteristics:

  • Requires JavaScript
  • Registered via customElements.define()
  • Matches :defined pseudo-class after registration
  • Can use Shadow DOM for style encapsulation
  • Can have custom properties, methods, lifecycle hooks

Best for:

  • Interactive components requiring JS
  • Reusable across different projects
  • Components needing encapsulated styles
  • Complex state management

When to Choose Each

FactorCSS-OnlyFull Web Component
JavaScript requiredNoYes
Works without JSYesNeeds fallback
Style encapsulationNo (global CSS)Yes (Shadow DOM)
ComplexityLowHigher
InteractivityCSS-only (:has, :checked)Full JS capability
Browser default displayinline (needs reset)Controlled in :host
Progressive enhancementNaturalRequires planning

Styling Considerations

CSS-only elements need explicit display:

The shared reset (_reset.css) handles this automatically with:

:not(:defined) {
  display: block;
}

If you need a different display value, override it in your component CSS:

product-card {
  display: grid;  /​* Overrides the reset's block */
}

Web Components control their own display:

// Inside the component
this.shadowRoot.innerHTML = `
  <style>
    :host {
      display: block;
      /​* Component defines its own display */
    }
  </​style>
  ...
`;

The x-* Pattern for Ad-Hoc Elements

For one-off custom elements, use the x-* prefix (e.g., <x-highlight>). These:

  • Are excluded from HTML validation by default
  • Work well as CSS-only elements
  • Signal "ad-hoc, not reusable" to other developers
<p>This is <x-highlight>important text</​x-highlight> in a paragraph.</​p>
x-highlight {
  background: var(--warning-light);
  padding-inline: var(--spacing-xs);
}

Related Skills

  • javascript-author - Write vanilla JavaScript for Web Components with function...
  • data-attributes - Using data-* attributes as the HTML/​CSS/​JS bridge for sta...
  • state-management - Client-side state patterns for Web Components
  • accessibility-checker - Ensure WCAG2AA accessibility compliance