Web Components development with Lit including custom elements, reactive properties, shadow DOM, and interoperability.
Creates Web Components using Lit with reactive properties, shadow DOM, and framework interoperability.
npx claudepluginhub a5c-ai/babysitterThis skill is limited to using the following tools:
README.mdExpert assistance for building Web Components with Lit.
Invoke this skill when you need to:
| Parameter | Type | Required | Description |
|---|---|---|---|
| elementName | string | Yes | Custom element name (must have hyphen) |
| properties | array | No | Reactive properties |
| shadowDom | boolean | No | Use shadow DOM (default: true) |
| typescript | boolean | No | Use TypeScript (default: true) |
// src/components/user-card.ts
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
@customElement('user-card')
export class UserCard extends LitElement {
static styles = css`
:host {
display: block;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
}
.user-card {
display: flex;
gap: 1rem;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
overflow: hidden;
background: #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.initials {
font-weight: bold;
font-size: 1.25rem;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
button.primary {
background: #3b82f6;
color: white;
}
`;
@property({ type: Object })
user!: User;
@property({ type: Boolean })
editable = false;
@state()
private isEditing = false;
@state()
private editedName = '';
private get initials() {
return this.user.name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase();
}
private startEditing() {
this.editedName = this.user.name;
this.isEditing = true;
}
private save() {
this.dispatchEvent(new CustomEvent('user-updated', {
detail: { ...this.user, name: this.editedName },
bubbles: true,
composed: true,
}));
this.isEditing = false;
}
private cancel() {
this.isEditing = false;
}
render() {
return html`
<div class="user-card">
<div class="avatar">
${this.user.avatar
? html`<img src=${this.user.avatar} alt=${this.user.name} />`
: html`<span class="initials">${this.initials}</span>`}
</div>
<div class="info">
${this.isEditing
? html`
<input
.value=${this.editedName}
@input=${(e: Event) => this.editedName = (e.target as HTMLInputElement).value}
@keydown=${(e: KeyboardEvent) => e.key === 'Enter' && this.save()}
/>
<button class="primary" @click=${this.save}>Save</button>
<button @click=${this.cancel}>Cancel</button>
`
: html`
<h2>${this.user.name}</h2>
<p>${this.user.email}</p>
${this.editable
? html`<button @click=${this.startEditing}>Edit</button>`
: null}
`}
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'user-card': UserCard;
}
}
// src/controllers/fetch-controller.ts
import { ReactiveController, ReactiveControllerHost } from 'lit';
export class FetchController<T> implements ReactiveController {
host: ReactiveControllerHost;
data: T | null = null;
loading = false;
error: Error | null = null;
private abortController: AbortController | null = null;
constructor(host: ReactiveControllerHost, private url: string) {
(this.host = host).addController(this);
}
hostConnected() {
this.fetch();
}
hostDisconnected() {
this.abortController?.abort();
}
async fetch() {
this.abortController?.abort();
this.abortController = new AbortController();
this.loading = true;
this.error = null;
this.host.requestUpdate();
try {
const response = await fetch(this.url, {
signal: this.abortController.signal,
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
this.data = await response.json();
} catch (e) {
if ((e as Error).name !== 'AbortError') {
this.error = e as Error;
}
} finally {
this.loading = false;
this.host.requestUpdate();
}
}
}
// Usage in component
@customElement('user-list')
export class UserList extends LitElement {
private usersController = new FetchController<User[]>(this, '/api/users');
render() {
if (this.usersController.loading) {
return html`<p>Loading...</p>`;
}
if (this.usersController.error) {
return html`<p>Error: ${this.usersController.error.message}</p>`;
}
return html`
<ul>
${this.usersController.data?.map(
user => html`<li>${user.name}</li>`
)}
</ul>
`;
}
}
// src/directives/highlight.ts
import { Directive, directive, PartInfo } from 'lit/directive.js';
class HighlightDirective extends Directive {
render(text: string, query: string) {
if (!query) return text;
const regex = new RegExp(`(${query})`, 'gi');
const parts = text.split(regex);
return parts.map(part =>
regex.test(part)
? document.createElement('mark').textContent = part
: part
);
}
}
export const highlight = directive(HighlightDirective);
// Usage
html`<p>${highlight(text, searchQuery)}</p>`
@customElement('modal-dialog')
export class ModalDialog extends LitElement {
static styles = css`
:host {
display: none;
}
:host([open]) {
display: block;
}
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.dialog {
background: white;
padding: 1.5rem;
border-radius: 0.5rem;
max-width: 500px;
width: 100%;
}
`;
@property({ type: Boolean, reflect: true })
open = false;
private handleBackdropClick(e: Event) {
if (e.target === e.currentTarget) {
this.dispatchEvent(new CustomEvent('close'));
}
}
render() {
return html`
<div class="backdrop" @click=${this.handleBackdropClick}>
<div class="dialog" role="dialog" aria-modal="true">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</div>
`;
}
}
// Usage
html`
<modal-dialog ?open=${showModal} @close=${() => showModal = false}>
<h2 slot="header">Confirm Action</h2>
<p>Are you sure you want to proceed?</p>
<div slot="footer">
<button @click=${confirm}>Confirm</button>
<button @click=${cancel}>Cancel</button>
</div>
</modal-dialog>
`
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.