Lit
Purpose
Build standard web components with Lit: reactive properties, shadow DOM templates, typed events, and SSR-compatible rendering.
Agent Protocol
Trigger
Exact user phrases: "Lit", "LitElement", "lit-html", "web component Lit", "reactive element", "@lit/reactive-element", "Lit SSR", "Lit component", "Lit decorator", "Lit reactive property".
Input Context
Before activating, verify:
- package.json has lit dependency.
- Whether the project uses decorators (experimentalDecorators) or the reactive-element base class.
- Whether Lit SSR (@lit-labs/ssr or @lit/react) is needed.
Output Artifact
No file output. Produces component design, property configuration, template patterns, and SSR setup as text.
Response Format
Component: {name} — {purpose}
Properties: {reactive attributes/properties}
Template: {shadow DOM structure}
Events: {CustomEvent dispatch}
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output.
Completion Criteria
- Components extend LitElement or ReactiveElement.
- Reactive properties defined with @property() decorator or static properties.
- Templates use Lit's html tagged template literal.
- Shadow DOM enabled (default in LitElement).
- Events dispatched as typed CustomEvent.
- Lit SSR configured for server-side rendering if needed.
- Styles scoped via shadow DOM or adoptedStyleSheets.
Max Response Length
2560 tokens.
Component Architecture / Decision Trees
Architecture Options
| Approach | Trade-off | When to Use |
|---|---|---|
| LitElement | Full API (render, styles, lifecycle) | Most components |
| ReactiveElement | No template system, minimal | When bundle is critical |
| @lit-labs/ssr | Server rendering + hydration | SSR-required projects |
| @lit/react | React wrapper generation | React design systems |
Base Class Decision
Do you need the full LitElement API (render, styles, lifecycle)?
Yes -> LitElement (default)
No -> Is bundle size critical?
Yes -> ReactiveElement (no template system, manual DOM management)
No -> LitElement for maintainability
Do you need SSR?
Yes -> LitElement + @lit-labs/ssr
No -> LitElement (default)
Reactive Property Configuration
Is the property part of the public API?
Yes -> @property() — exposes as HTML attribute, triggers update
No -> @state() — internal only, triggers update but no attribute
How should the attribute be named?
CamelCase property -> 'my-prop' attribute (auto kebab-case)
Need custom name -> attribute: 'custom-name'
No attribute needed -> attribute: false
Should the attribute reflect changes back to DOM?
Yes -> reflect: true (useful for CSS attribute selectors)
No -> reflect: false (default, better performance)
Styling Strategy
How should styles be scoped?
Shadow DOM -> static styles in LitElement (default, best encapsulation)
Light DOM -> override createRenderRoot() for shared styles
Adopted stylesheets -> adoptedStyleSheets for performance (multiple instances)
CSS custom properties -> ::part() for component customization API
Component Design Patterns
Basic Component with Properties
import { LitElement, html, css } from 'lit'
import { property, state } from 'lit/decorators.js'
export class MyCounter extends LitElement {
static styles = css`
:host { display: block; padding: 1rem; }
button { cursor: pointer; }
`
@property({ type: Number }) initial = 0
@state() private count = 0
connectedCallback() {
super.connectedCallback()
this.count = this.initial
}
render() {
return html`
<p>Count: ${this.count}</p>
<button @click=${() => this.count++}>+</button>
<button @click=${() => this.count--}>-</button>
`
}
}
Component with Typed Events
import { LitElement, html } from 'lit'
import { property } from 'lit/decorators.js'
export class MyDropdown extends LitElement {
@property({ type: Array }) options: string[] = []
@property({ type: String }) value = ''
private select(option: string) {
this.value = option
this.dispatchEvent(new CustomEvent('select', {
detail: option,
bubbles: true,
composed: true,
}))
}
render() {
return html`
<div @click=${(e: Event) => {
const target = e.target as HTMLElement
if (target.dataset.value) this.select(target.dataset.value)
}}>
${this.options.map(o => html`
<div data-value=${o} class=${o === this.value ? 'active' : ''}>
${o}
</div>
`)}
</div>
`
}
}
Renderless Controller Pattern
import { ReactiveController, ReactiveControllerHost } from 'lit'
export class ResizeController implements ReactiveController {
private entries: ResizeObserverEntry[] = []
private observer: ResizeObserver | null = null
constructor(private host: ReactiveControllerHost) {
this.host.addController(this)
}
hostConnected() {
this.observer = new ResizeObserver(entries => {
this.entries = entries
this.host.requestUpdate()
})
this.observer.observe(this.host as HTMLElement)
}
hostDisconnected() {
this.observer?.disconnect()
}
}
State Management Patterns
Local State with @state
@state() private visible = false
@state() private items: Item[] = []
Derived State via Getter
get total() { return this.items.reduce((s, i) => s + i.price, 0) }
get count() { return this.items.length }
Reactive Controller for Shared Logic
Controllers encapsulate stateful behavior (resize observers, intersection observers, form state) and can be reused across components.
Performance Optimization
Rendering Performance
- lit-html: templates parsed once, cloned on each render.
- Only dynamic parts (expressions) updated, not entire template.
- No virtual DOM diffing — direct DOM manipulation at expression level.
repeat()directive uses key-based identity for efficient list updates.
Bundle Size
- Lit runtime: ~5KB gzipped (no dependencies).
- ReactiveElement: ~3KB without template system.
- No JSX runtime needed — templates are standard tagged templates.
Optimization Techniques
- Use
@property({ hasChanged })to customize change detection. - Use
shouldUpdate()to skip renders when inputs haven't meaningfully changed. - Batch property changes — Lit automatically batches updates via microtask.
- Use
adoptedStyleSheetsfor shared styles across many component instances.
Build & Bundle Considerations
- Lit uses standard ES modules — works with any bundler (Rollup, webpack, Vite).
litpackage includes lit-html and LitElement in one import.- For tree-shaking: import only what you use from
lit/directives/*. - Production build: minify + bundle with Rollup or Vite.
- SSR:
@lit-labs/ssrfor Node.js rendering,@lit-labs/ssr-clientfor hydration. @lit/reactcreates React wrappers for Lit components.- Use
@lit/localizefor internationalization.
Testing Strategies
Unit Testing with Web Test Runner
import { fixture, assert } from '@open-wc/testing'
import './my-element.js'
describe('MyElement', () => {
it('renders with default properties', async () => {
const el = await fixture('<my-element></my-element>')
assert.equal(el.shadowRoot?.querySelector('button')?.textContent?.trim(), 'Click me')
})
it('reacts to property changes', async () => {
const el = await fixture<MyElement>('<my-element></my-element>')
el.count = 5
await el.updateComplete
assert.include(el.shadowRoot?.textContent ?? '', '5')
})
})
Event Testing
it('dispatches custom event on button click', async () => {
const el = await fixture<MyElement>('<my-element></my-element>')
const handler = sinon.spy()
el.addEventListener('my-event', handler)
el.shadowRoot?.querySelector('button')?.click()
assert.isTrue(handler.calledOnce)
assert.equal(handler.firstCall.args[0].detail, 'clicked')
})
Key Testing Practices
- Use
@open-wc/testingfor test helpers (fixture, assert, waitFor). - Wait for
el.updateCompletebefore asserting after property changes. - Test property → attribute reflection and attribute → property deserialization.
- Test shadow DOM queries with
el.shadowRoot.querySelector.
Migration Patterns
From Vanilla Custom Elements to Lit
| Vanilla | Lit |
|---|---|
class MyEl extends HTMLElement |
class MyEl extends LitElement |
observedAttributes() |
@property() decorator |
attributeChangedCallback() |
willUpdate() / updated() |
connectedCallback() + manual render |
render() auto-called |
this.innerHTML = template |
return html\...`` |
| Manual style attachment | static styles = css\...`` |
Migration order: 1) Change extends to LitElement, 2) Replace observedAttributes with @property, 3) Replace innerHTML with render(), 4) Add static styles, 5) Convert lifecycle methods.
From React to Lit
| React Concept | Lit Equivalent |
|---|---|
useState |
@state() property |
useEffect |
updated() / willUpdate() |
useMemo |
shouldUpdate() for control |
| JSX | html tagged template literal |
| Props | @property() decorator |
| Event callbacks | CustomEvent dispatch |
Anti-Patterns
- Forgetting to call super on lifecycle:
super.connectedCallback(),super.disconnectedCallback(),super.updated(). - Imperative DOM manipulation in render(): render() should be pure — side effects go in updated().
- Large template literals: Break templates into helper functions or sub-components.
- Missing @state for internal state: Using @property for internal state exposes it as an HTML attribute.
- Events without composed: true: Events can't cross shadow DOM boundaries without composed: true.
- Not using lit-html directives: Directives (repeat, classMap, styleMap) optimize rendering vs manual DOM.
- Memory leaks from missing disconnectedCallback: Always clean up timers, observers, and event listeners.
- Shadow DOM a11y issues: ARIA attributes on host may not cross shadow boundary without :host.
Common Pitfalls
- Forgetting to call super on lifecycle — always call
super.connectedCallback(). - Missing @state for internal state — @property exposes it as HTML attribute.
- Events without composed: true — can't cross shadow boundaries.
- Large template literals — break into smaller functions.
- Memory leaks — always clean up in disconnectedCallback.
Compared With
| Aspect | Lit | Stencil | Vanilla WC |
|---|---|---|---|
| Bundle size | ~5KB | ~8KB | 0KB |
| Rendering | lit-html (template literal) | JSX (compiled) | Manual |
| Reactivity | @property/@state decorators | @Prop/@State decorators | attributeChangedCallback |
| SSR | @lit-labs/ssr | @stencil/core (SSR) | Manual |
| TypeScript | Full support | Required | Optional |
Template Directives Reference
| Directive | Purpose |
|---|---|
ifDefined |
Render attr only if defined |
classMap |
Toggle CSS classes |
styleMap |
Inline styles |
repeat |
Keyed list rendering |
when |
Conditional rendering |
until |
Promise placeholder |
live |
Attribute always matches live value |
keyed |
Force DOM reuse |
guard |
Memoize template parts |
cache |
Cache DOM across conditionals |
Lifecycle Reference
| Method | Purpose |
|---|---|
connectedCallback() |
Element added to DOM |
disconnectedCallback() |
Element removed from DOM |
willUpdate(changed) |
Before render |
update(changed) |
Before render (read prop values) |
render() |
Return lit-html template |
updated(changed) |
After render |
firstUpdated(changed) |
First render only |
shouldUpdate(changed) |
Control whether render fires |
Tooling
- Lit VS Code Extension — syntax highlighting
@lit/reactive-element— base class without template@lit-labs/ssr— server-side rendering@lit/react— React wrapper generation@lit/localize— i18nlit-analyzer— type checking@open-wc/testing— testing utilities@web/test-runner— test runner- Storybook for Lit — component development
Ecosystem
UI Libraries Built with Lit
- Shoelace — Most popular Lit library
- Material Web — Google's MD3 components
- Wired Elements — Hand-drawn style
- Vaadin Components — Enterprise UI
Integration Patterns
- React:
@lit/reactcreates wrappers - Vue: Native custom elements
- Angular: Custom Elements Schema
- Svelte:
<svelte:options tag="my-el" />
Rules
- Extend LitElement for full feature set, ReactiveElement for minimal footprint.
- Use @property decorator for public API, @state for internal state.
- Shadow DOM is default — use
createRenderRoot()override for light DOM only when unavoidable. - Events use CustomEvent with typed
detail— always setbubblesandcomposed. - Styles are scoped via static
styles— avoid global leak from shadow DOM. - Use lit-html directives (
repeat,classMap,ifDefined,when) over imperative DOM.
References
- references/lit-advanced.md — Lit Advanced
- references/lit-architecture.md — Lit Architecture Patterns
- references/lit-deployment.md — Lit Deployment
- references/lit-essentials.md — Lit Essentials
- references/lit-fundamentals.md — Lit Fundamentals
- references/lit-testing.md — Lit Testing Reference
Handoff
No artifact produced. Next skill: frontend-universal-web-components for vanilla custom elements and cross-framework compatibility. Carry forward: LitElement patterns, reactive property config, shadow DOM conventions.
Implementation Patterns
Factory Pattern for Module Creation
function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }
Builder Pattern for Complex Configuration
class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }
Production Considerations
Deployment Checklist
- Production build with optimizations enabled
- Environment variables configured per environment
- Health check endpoint responds correctly
- Error tracking and monitoring integrated
- Logging level configured (not debug in production)
- Resource limits configured
- Database migrations applied
- Static assets built and served from CDN or cache
- Feature flags toggled appropriately
- Rollback plan documented and tested
Monitoring and Alerting
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% | Critical | Rollback or fix |
| p95 latency | > 500ms | Warning | Profile and optimize |
| Uptime | < 99.9% | Critical | Investigate infrastructure |
| Memory usage | > 80% | Warning | Check for leaks |
| CPU usage | > 80% | Warning | Scale up or optimize |
Security Considerations
Threat Modeling (STRIDE)
- Spoofing: Identity validation, authentication
- Tampering: Integrity checks, digital signatures
- Repudiation: Audit logs, non-repudiation
- Information disclosure: Encryption, access control
- Denial of service: Rate limiting, resource quotas
- Elevation of privilege: Principle of least privilege
Supply Chain Security
- Dependency scanning: Snyk, Dependabot, Trivy
- SBOM generation: CycloneDX or SPDX format
- Signed commits: GPG or SSH commit signing
- Artifact verification: Checksum validation, signature verification
Secrets Management
- Secrets never in code — always in secrets manager (Vault, AWS Secrets Manager)
- Rotation policy: Rotate database credentials every 90 days
- Access audit: Log every secrets access, alert on anomalies
- Encryption at rest and in transit for all secrets
- Principle of least privilege: each service gets only its own secrets
Architecture Decision Trees
Component Design Decision Tree
Does component render dynamic content from properties?
├── No → Pure template with static styles
└── Yes → Are properties primitive values?
├── Yes → @property() with type converter
└── No → @property() with custom converter or @state()
Does the component need to render children/slots?
├── Yes → Use <slot> elements in shadow DOM
└── No → Closed shadow DOM for encapsulated components
Reactive Strategy Decision Tree
Does the component need to react to external state?
├── No → Static component, no reactive updates needed
└── Yes → Is the state from DOM events?
├── Yes → @eventOptions + this.requestUpdate()
└── No → Is the state coming from an observable?
├── Yes → Use LitElement + rxjs with connect() pattern
└── No → Use @property decorators + willUpdate lifecycle