con un clic
custom-elements
Define and use custom HTML elements. Use when creating new components, defining custom tags, or using project-specific elements beyond standard HTML5.
Menú
Define and use custom HTML elements. Use when creating new components, defining custom tags, or using project-specific elements beyond standard HTML5.
Write vanilla JavaScript for Web Components with functional core, imperative shell. Use when creating JavaScript files, building interactive components, or writing any client-side code.
Modern CSS organization with native @import, @layer cascade control, CSS nesting, design tokens, and element-focused selectors. AUTO-INVOKED when editing .css files.
Enforce structured git workflow with conventional commits, feature branches, semver versioning, and work logging. Use for all code changes to prevent work loss and maintain history.
INVOKE FIRST before any code work. Validates git workflow (branch, issue, worklog) and checks approach. Use at START of every task and END before completing. Prevents skipped steps.
Use consistent Lucide icons with the <icon-wc> component. Use when adding icons to pages, buttons, or UI elements.
Umbrella coordinator for image handling. Coordinates responsive-images, placeholder-images, and automation scripts. Use when adding images to any page, optimizing existing images, or setting up image pipelines.
| name | custom-elements |
| description | Define and use custom HTML elements. Use when creating new components, defining custom tags, or using project-specific elements beyond standard HTML5. |
| allowed-tools | Read, Write, Edit, Bash |
This skill provides guidance for defining and using custom HTML elements in this project.
| System | File | Purpose |
|---|---|---|
| HTML Validation | .claude/schemas/elements.json | Validates custom elements in HTML (html-validate) |
| Custom Elements Manifest | custom-elements.json | Documents 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.
Check .claude/schemas/elements.json for defined custom elements:
| Element | Type | Purpose |
|---|---|---|
product-card | Block | Product display with sku, price attributes |
icon-element | Void | Self-closing icon with required name attribute |
user-avatar | Void | Avatar with required src, alt and optional size |
status-badge | Inline | Status indicator with type (success/warning/error/info) |
data-table | Block | Data table with source, sortable attributes |
nav-menu | Block | Navigation with orientation (horizontal/vertical) |
<!-- 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>
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.
/add-element my-widget
Add to .claude/schemas/elements.json:
{
"my-element": {
"flow": true,
"phrasing": false,
"permittedContent": ["@flow"],
"attributes": {
"required-attr": { "required": true },
"optional-attr": { "required": false }
}
}
}
| Property | Values | Description |
|---|---|---|
flow | boolean | Can appear where flow content is expected |
phrasing | boolean | Can appear where phrasing content is expected |
void | boolean | Self-closing element (no content) |
permittedContent | array | What content is allowed inside |
@flow - Flow content (most elements)@phrasing - Phrasing content (inline elements)@interactive - Interactive elements["p", "div"] - Specific elements only{
"attributes": {
"name": {
"required": true // Must be present
},
"type": {
"required": false,
"enum": ["a", "b", "c"] // Restricted values
},
"enabled": {
"boolean": true // Boolean attribute
}
}
}
{
"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>
{
"loading-spinner": {
"void": true,
"flow": true,
"phrasing": true,
"attributes": {
"size": {
"enum": ["small", "medium", "large"]
}
}
}
}
Usage:
<loading-spinner size="medium"/>
{
"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>
my-elementproduct-card not pcform-input, form-select, form-buttonCustom elements can be used in two distinct ways. Choose based on your needs.
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(--size-l);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
Characteristics:
customElements.define() call:not(:defined) pseudo-classdisplay: block (handled by shared reset)Best for:
.card classes)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:
customElements.define():defined pseudo-class after registrationBest for:
| Factor | CSS-Only | Full Web Component |
|---|---|---|
| JavaScript required | No | Yes |
| Works without JS | Yes | Needs fallback |
| Style encapsulation | No (global CSS) | Yes (Shadow DOM) |
| Complexity | Low | Higher |
| Interactivity | CSS-only (:has, :checked) | Full JS capability |
| Browser default display | inline (needs reset) | Controlled in :host |
| Progressive enhancement | Natural | Requires planning |
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>
...
`;
For one-off custom elements, use the x-* prefix (e.g., <x-highlight>). These:
<p>This is <x-highlight>important text</x-highlight> in a paragraph.</p>
x-highlight {
background: var(--warning-light);
padding-inline: var(--size-2xs);
}