Claude Code for Lit: Web Components with Reactive Properties — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Lit: Web Components with Reactive Properties
Frontend

Claude Code for Lit: Web Components with Reactive Properties

Published: February 3, 2027
Read time: 8 min read
By: Claude Skills 360

Lit builds web components on the browser’s Custom Elements, Shadow DOM, and HTML Templates standards — components that work in any framework or plain HTML without a runtime. LitElement extends HTMLElement with a declarative reactive system: @property() declares observed attributes, @state() declares private reactive state, and the html tagged template literal renders efficiently with minimal DOM updates. css tagged templates scope styles to the shadow DOM. @query and @queryAll access shadow DOM nodes. Lit’s event system uses standard DOM custom events. Components published to npm work in React, Vue, Angular, Svelte, or vanilla HTML without adapters. Claude Code generates Lit web components, reactive property definitions, template directives, CSS-in-components, and the Vite configurations for building distributable Lit component libraries.

CLAUDE.md for Lit Projects

## Lit Stack
- Version: lit >= 3.1 (lit@3 = LitElement + lit-html + reactive-element)
- Components: class OrderCard extends LitElement — @property for external, @state for internal
- Templates: html`<div>${value}</div>` — tagged template, efficient DOM diff
- Styles: static styles = css`...` — scoped to shadow DOM
- Events: @property type dispatches — this.dispatchEvent(new CustomEvent(...))
- Directives: repeat(), classMap(), styleMap(), ifDefined(), cache() from lit/directives
- SSR: @lit-labs/ssr for server rendering + hydration
- Build: Vite with NPM for library output — single bundle for consumption

LitElement Component

// src/components/order-card.ts — Lit web component
import { LitElement, html, css, PropertyValues } from "lit"
import { customElement, property, state, query } from "lit/decorators.js"
import { classMap } from "lit/directives/class-map.js"
import { when } from "lit/directives/when.js"
import { repeat } from "lit/directives/repeat.js"

interface Order {
  id: string
  status: "pending" | "processing" | "shipped" | "delivered" | "cancelled"
  totalCents: number
  items: { name: string; quantity: number; priceCents: number }[]
  createdAt: string
}

@customElement("order-card")
export class OrderCard extends LitElement {
  // Scoped styles — not leaked to document
  static styles = css`
    :host {
      display: block;
      font-family: system-ui, sans-serif;
    }
    :host([hidden]) {
      display: none;
    }
    .card {
      border: 1px solid #e2e8f0;
      border-radius: 8px;
      padding: 16px;
      transition: box-shadow 0.2s;
    }
    .card:hover {
      box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    }
    .card.highlighted {
      border-color: #3b82f6;
    }
    .status {
      display: inline-block;
      padding: 2px 8px;
      border-radius: 999px;
      font-size: 0.75rem;
      font-weight: 500;
    }
    .status.pending { background: #fef3c7; color: #92400e; }
    .status.shipped { background: #d1fae5; color: #065f46; }
    .status.cancelled { background: #fee2e2; color: #991b1b; }
    .items { margin-top: 12px; }
    .item { display: flex; justify-content: space-between; padding: 4px 0; }
    button { cursor: pointer; padding: 6px 12px; border-radius: 6px; border: none; }
    .cancel-btn { background: #fee2e2; color: #991b1b; }
  `

  // @property: reflected to HTML attribute, triggers re-render
  @property({ type: Object }) order: Order | null = null
  @property({ type: Boolean }) highlighted = false
  @property({ type: Boolean, reflect: true }) expanded = false

  // @state: private reactive state, not reflected
  @state() private cancelling = false
  @state() private showItems = false

  // @query: access shadow DOM node
  @query(".card") private cardElement!: HTMLElement

  // Lifecycle: called when properties change
  protected willUpdate(changedProperties: PropertyValues) {
    if (changedProperties.has("order")) {
      this.cancelling = false
    }
  }

  protected render() {
    if (!this.order) return html`<div class="card">No order</div>`

    const { id, status, totalCents, items, createdAt } = this.order

    return html`
      <div class=${classMap({ card: true, highlighted: this.highlighted })}>
        <div style="display:flex; justify-content:space-between; align-items:center">
          <span style="font-family:monospace; font-size:0.875rem">#${id.slice(-8)}</span>
          <span class="status ${status}">${status}</span>
        </div>

        <div style="margin-top:8px">
          <strong>$${(totalCents / 100).toFixed(2)}</strong>
          <span style="color:#6b7280; font-size:0.875rem">
            · ${new Date(createdAt).toLocaleDateString()}
          </span>
        </div>

        ${when(
          this.showItems,
          () => html`
            <div class="items">
              ${repeat(
                items,
                item => item.name,
                item => html`
                  <div class="item">
                    <span>${item.name} ×${item.quantity}</span>
                    <span>$${((item.priceCents * item.quantity) / 100).toFixed(2)}</span>
                  </div>
                `
              )}
            </div>
          `
        )}

        <div style="display:flex; gap:8px; margin-top:12px">
          <button @click=${this.toggleItems}>
            ${this.showItems ? "Hide" : "Show"} items
          </button>

          ${when(
            status === "pending",
            () => html`
              <button
                class="cancel-btn"
                ?disabled=${this.cancelling}
                @click=${this.handleCancel}
              >
                ${this.cancelling ? "Cancelling..." : "Cancel"}
              </button>
            `
          )}
        </div>

        <!-- Named slot for additional content -->
        <slot name="footer"></slot>
      </div>
    `
  }

  private toggleItems() {
    this.showItems = !this.showItems
  }

  private async handleCancel() {
    this.cancelling = true

    // Dispatch custom event — bubbles up through DOM
    this.dispatchEvent(
      new CustomEvent<{ orderId: string }>("order-cancel", {
        detail: { orderId: this.order!.id },
        bubbles: true,
        composed: true,  // Cross shadow DOM boundary
      })
    )
  }

  // Public method callable from outside
  focus() {
    this.cardElement?.focus()
  }
}

// TypeScript declarations for JSX/template consumers
declare global {
  interface HTMLElementTagNameMap {
    "order-card": OrderCard
  }
}

Order List Component with Async Data

// src/components/order-list.ts
import { LitElement, html, css } from "lit"
import { customElement, property, state } from "lit/decorators.js"
import { Task } from "@lit/task"
import { repeat } from "lit/directives/repeat.js"
import "./order-card.js"

@customElement("order-list")
export class OrderList extends LitElement {
  static styles = css`
    :host { display: block; }
    .grid { display: grid; gap: 12px; }
    .empty { text-align: center; color: #9ca3af; padding: 48px; }
  `

  @property() customerId = ""

  // @lit/task: declarative async task with loading/error states
  private _ordersTask = new Task(this, {
    task: async ([customerId]: [string], { signal }) => {
      if (!customerId) return []

      const response = await fetch(
        `/api/orders?customerId=${customerId}`,
        { signal }
      )
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      return response.json() as Promise<Order[]>
    },
    args: () => [this.customerId],
  })

  protected render() {
    return this._ordersTask.render({
      pending: () => html`<p>Loading orders...</p>`,
      error: (e) => html`<p>Error: ${(e as Error).message}</p>`,
      complete: (orders) => {
        if (orders.length === 0) {
          return html`<div class="empty">No orders found</div>`
        }
        return html`
          <div class="grid">
            ${repeat(
              orders,
              order => order.id,
              order => html`
                <order-card
                  .order=${order}
                  @order-cancel=${this.handleCancel}
                ></order-card>
              `
            )}
          </div>
        `
      },
    })
  }

  private async handleCancel(event: CustomEvent<{ orderId: string }>) {
    const { orderId } = event.detail
    await fetch(`/api/orders/${orderId}/cancel`, { method: "POST" })
    this._ordersTask.run()  // Refresh
  }
}

Using Lit in React

// React wrapper — Lit works directly via ref + event listeners
import { useRef, useEffect } from "react"
import "@/components/order-card"  // Register custom element

interface OrderCardWrapperProps {
  order: Order
  highlighted?: boolean
  onCancel: (orderId: string) => void
}

export function OrderCardWrapper({ order, highlighted, onCancel }: OrderCardWrapperProps) {
  const ref = useRef<HTMLElement>(null)

  useEffect(() => {
    const el = ref.current
    if (!el) return

    const handler = (e: Event) => {
      const detail = (e as CustomEvent<{ orderId: string }>).detail
      onCancel(detail.orderId)
    }

    el.addEventListener("order-cancel", handler)
    return () => el.removeEventListener("order-cancel", handler)
  }, [onCancel])

  return (
    // @ts-ignore — custom element not in JSX types
    <order-card
      ref={ref}
      highlighted={highlighted}
    />
  )
}

Vite Library Build Config

// vite.config.ts — build as npm-distributable component library
import { defineConfig } from "vite"
import { resolve } from "path"

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, "src/index.ts"),
      formats: ["es"],
      fileName: "index",
    },
    rollupOptions: {
      // Don't bundle Lit — consumers provide it
      external: ["lit", "lit/decorators.js", "lit/directives/*.js", "@lit/task"],
      output: {
        preserveModules: true,
        preserveModulesRoot: "src",
      },
    },
  },
})
// src/index.ts — library entry point
export { OrderCard } from "./components/order-card.js"
export { OrderList } from "./components/order-list.js"
export type { Order } from "./types.js"

For the React component library alternative when framework-locking is acceptable and the React ecosystem (Storybook, Testing Library, RSC) is more valuable than portability, the React component patterns guide covers compound components and render props. For the Stencil.js compiler that generates Lit-style web components plus framework-specific wrappers (React, Vue, Angular) from a single component, the component library guide covers multi-framework distribution. The Claude Skills 360 bundle includes Lit skill sets covering LitElement components, directives, and library builds. Start with the free tier to try Lit web component generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free