---
description: Lit web components conventions
globs: ["src/**/*.ts"]
alwaysApply: false
---

# Lit 3 web components

- One element per file: `@customElement('rs-button') export class RsButton extends LitElement`. Tag names use the project prefix and a hyphen. Add `declare global { interface HTMLElementTagNameMap { 'rs-button': RsButton } }`.
- Public API via `@property()` (with `type` and `reflect: true` only when styling needs the attribute); internal state via `@state()`. Boolean attributes default to `false`.
- Don't mutate objects/arrays in place; assign a new value so Lit sees the change.
- Templates use the `html` tagged template; bind events with `@click=${this.handleClick}`, properties with `.value=${...}`, booleans with `?disabled=${...}`. Use `repeat()` with a key for lists and `nothing` for empty output.
- Never insert untrusted HTML with `unsafeHTML`.
- Styles go in `static styles` using the `css` tagged template; expose theming via CSS custom properties (`--rs-button-bg`) and `::part()`; don't depend on global CSS.
- Communicate upward with `this.dispatchEvent(new CustomEvent('rs-change', { detail, bubbles: true, composed: true }))`. Document events and properties in JSDoc (for the custom elements manifest).
- Lifecycle: `connectedCallback`/`disconnectedCallback` (always call `super`) for global listeners; `willUpdate` for derived state; `updated` for post-render DOM work. Reuse logic with reactive controllers.
- Forms: form-associated elements with `static formAssociated = true` and `ElementInternals` when the component is an input.
- Accessibility: semantic elements inside the shadow root, `delegatesFocus` for focusable wrappers, labels passed through.

## Done when
Typecheck, lint, and `@web/test-runner` (or Vitest browser mode) tests pass.
