Build Lit web components using composition API with defineElement. Supports lifecycle hooks, props, shadow/light DOM, and optional dependency injection.
Build Lit web components using the composition API provided by the lit-composition library. This skill helps you create components with `defineElement()`, manage lifecycle hooks, configure props, and choose between shadow/light DOM rendering.
Creates Lit web components using the lit-composition library's composition-style API. Components are defined via `defineElement()` which returns a LitElement subclass and optionally registers it as a custom element. Supports two forms: object options with `setup()`/`render()` or functional shorthand.
The primary API is `defineElement(options)` from `src/defineElement/defineElement.ts`:
```ts
export const defineElement = (options: {
name?: string // custom element name (must include dash)
parent?: typeof LitElement // default: LitElement
styles?: CSSResultGroup
props?: Record<string, PropertyDeclaration>
register?: boolean // default: true if name present
shadowRoot?: boolean // default: true (false = light DOM)
setup?: (this: Instance, comp?: Instance) => void | (() => unknown)
render?: (this: Instance) => unknown
}) => LitElementSubclass
```
**Use `render()`** for purely declarative components without lifecycle needs:
```ts
import { html, css } from 'lit'
import { defineElement } from 'lit-composition'
export const HelloWorld = defineElement({
name: 'hello-world',
styles: css`:host{display:block;padding:4px}`,
props: {
msg: {type: String, reflect: true},
},
render() {
return html`<div>${this.msg ?? 'Hello, world!'}</div>`
},
})
```
**Use `setup()`** when you need hooks, subscriptions, or computed render logic:
```ts
import { html } from 'lit'
import { defineElement, onConnected, onUpdated } from 'lit-composition'
export const MyCounter = defineElement({
name: 'my-counter',
props: {count: {type: Number, reflect: true}},
setup() {
onConnected(() => {
console.log('connected')
})
onUpdated((changed) => {
if (changed.has('count')) {
console.log('count changed')
}
})
return function render() {
return html`<button @click=${() => this.count++}>Count: ${this.count ?? 0}</button>`
}
},
})
```
Available hooks (from `src/defineElement/hooks.ts`):
**Rules:**
Define props using Lit's `PropertyDeclaration` shape:
```ts
props: {
count: {type: Number, reflect: true},
mode: {type: String as PropType<'a'|'b'>, attribute: true},
disabled: {type: Boolean, reflect: true}
}
```
Defaults are assigned after `setup()` via `assignDefaultValues()`. Set them in `props` or imperatively in `setup()`.
**Default:** Shadow DOM (standard Lit behavior)
**Light DOM:** Set `shadowRoot: false` to render into the host element:
```ts
export const Clicker = defineElement({
name: 'my-clicker',
shadowRoot: false,
props: {count: {type: Number}},
setup() {
onConnected(() => console.log('connected'))
return () => html`<button @click=${() => this.count = (this.count ?? 0) + 1}>${this.count ?? 0}</button>`
},
})
```
**Note:** In light DOM mode, you must handle style scoping manually.
Provide `styles` with `CSSResultGroup` (Lit's `css` tagged literal):
```ts
styles: css`
:host {
display: block;
padding: 16px;
}
button {
background: blue;
color: white;
}
`
```
For dependency injection, use `provide` and `inject` from `src/context/`:
```ts
import { provide, inject } from 'lit-composition/context'
// Provider component
setup() {
provide('theme', {mode: 'dark'})
}
// Consumer component
setup() {
const theme = inject('theme')
return () => html`<div>Mode: ${theme.mode}</div>`
}
```
**Requirements:**
**Unit tests (Vitest):**
Place tests next to modules in `tests/`:
```ts
import { expect, test } from 'vitest'
import { MyCounter } from './MyCounter'
test('increments count on click', async () => {
const el = document.createElement('my-counter')
document.body.appendChild(el)
await el.updateComplete
const button = el.shadowRoot.querySelector('button')
button.click()
await el.updateComplete
expect(el.count).toBe(1)
})
```
**Run tests:** `pnpm test`
Before committing changes:
```ts
import { html, css } from 'lit'
import { defineElement, onConnected, onUpdated } from 'lit-composition'
export const TodoItem = defineElement({
name: 'todo-item',
styles: css`
:host {
display: block;
padding: 8px;
border-bottom: 1px solid #ddd;
}
.completed {
text-decoration: line-through;
opacity: 0.6;
}
`,
props: {
text: {type: String},
completed: {type: Boolean, reflect: true}
},
setup() {
onConnected(() => {
console.log('Todo item mounted')
})
onUpdated((changed) => {
if (changed.has('completed')) {
this.dispatchEvent(new CustomEvent('todo-toggle', {
detail: {completed: this.completed}
}))
}
})
return function render() {
return html`
<div class="${this.completed ? 'completed' : ''}">
<input
type="checkbox"
.checked=${this.completed}
@change=${() => this.completed = !this.completed}
/>
<span>${this.text}</span>
</div>
`
}
}
})
```
**Light DOM with hooks:**
```ts
defineElement({
name: 'my-el',
shadowRoot: false,
setup() {
onConnected(() => {/* ... */})
return () => html`<!-- ... -->`
}
})
```
**Typed props:**
```ts
props: {
mode: {type: String as PropType<'edit'|'view'>}
}
```
**Conditional rendering:**
```ts
return () => html`
${this.loading
? html`<spinner-el></spinner-el>`
: html`<content-el .data=${this.data}></content-el>`
}
`
```
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/lit-composition-component-builder/raw