Custom Elements
Custom elements (LitElement classes) provide reactive properties, lifecycle methods, and encapsulated state. They are Web Components that extend LitElement.
Basic Custom Element
Create a custom element by extending LitElement and using decorators:
import { LitElement } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
@customElement('counter-element')
class Counter extends LitElement {
@property({ type: Number }) count = 0
@state() private message = ''
private increment() {
this.count++
this.message = `Clicked ${this.count} times`
}
render() {
return (
<div>
<button onclick={() => this.increment()}>
Count: {this.count}
</button>
{this.message && <p>{this.message}</p>}
</div>
)
}
}
// Usage - tag name doesn't need static attribute
<counter-element count={5} />Using Custom Elements
Using the Registered Tag Name
When you register a custom element with @customElement('counter-element'), you can use the tag name directly:
// ✅ Using the registered tag name
<counter-element count={5} />
<my-button onclick={handleClick}>Click</my-button>Using the Class Directly
You can also use the class itself as a component, which requires the static attribute:
import { Counter } from './counter-element'
// ✅ Using class directly - requires static attribute
<Counter static={true} count={5} />
// You can also use the shorthand (but explicit form helps IntelliSense)
<Counter static count={5} />
// ❌ Without static - won't compile correctly
<Counter count={5} />TIP
You can enable automatic class detection by setting useImportDiscovery: true in your Vite config, which eliminates the need for the static attribute when using classes directly. This comes at the cost of slightly higher compilation time, as it needs to track the import back to its declaration.
Custom Element with Styles
Use the static styles property for component styles:
import { LitElement, css } from 'lit'
import { customElement, property } from 'lit/decorators.js'
@customElement('styled-button')
class StyledButton extends LitElement {
@property({ type: String }) label = 'Click me'
@property({ type: String }) variant: 'primary' | 'secondary' = 'primary'
static styles = css`
:host {
display: inline-block;
}
button {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.primary {
background: blue;
color: white;
}
.secondary {
background: gray;
color: white;
}
`
render() {
return (
<button class={this.variant}>
{this.label}
</button>
)
}
}
// Usage
<styled-button label="Submit" variant="primary" />Generic Custom Elements
Custom elements can use TypeScript generics for type-safe properties:
import { LitElement } from 'lit'
import { customElement, property } from 'lit/decorators.js'
@customElement('data-list')
class DataList<T> extends LitElement {
@property({ type: Array }) items: T[] = []
@property({ attribute: false }) renderItem!: (item: T) => JSX.Element
render() {
return (
<ul>
{this.items.map((item, index) => (
<li key={index}>{this.renderItem(item)}</li>
))}
</ul>
)
}
}
// Usage
<data-list
items={as.prop(users)}
renderItem={as.prop((user: User) => <span>{user.name}</span>)}
/>Generic Custom Element with Styles
import { LitElement, css } from 'lit'
import { customElement, property } from 'lit/decorators.js'
interface Selectable {
id: string
label: string
}
@customElement('selectable-list')
class SelectableList<T extends Selectable> extends LitElement {
@property({ type: Array }) items: T[] = []
@property({ type: String }) selectedId?: string
@property({ attribute: false }) onSelect?: (item: T) => void
static styles = css`
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
li:hover {
background: #f5f5f5;
}
li.selected {
background: #e3f2fd;
font-weight: bold;
}
`
private handleSelect(item: T) {
this.onSelect?.(item)
}
render() {
return (
<ul>
{this.items.map(item => (
<li
key={item.id}
class={item.id === this.selectedId ? 'selected' : ''}
onclick={() => this.handleSelect(item)}
>
{item.label}
</li>
))}
</ul>
)
}
}
// Usage
<selectable-list items={as.prop(items)} onSelect={as.prop(handleSelect)} />Declaring Custom Events for IntelliSense
When creating components that dispatch custom events, declare event handler properties using declare to get automatic IntelliSense:
import { LitElement } from 'lit'
import { customElement, property } from 'lit/decorators.js'
interface Todo {
id: string
text: string
completed: boolean
}
@customElement('todo-item')
class TodoItem extends LitElement {
@property({ type: Object }) todo!: Todo
// Use 'declare' for TypeScript-only properties that provide IntelliSense
// These won't exist at runtime but enable autocomplete for event handlers
declare ontoggle: ((e: CustomEvent<{ id: string }>) => void) | undefined
declare ondelete: ((e: CustomEvent<{ id: string }>) => void) | undefined
private handleToggle() {
this.dispatchEvent(new CustomEvent('toggle', {
detail: { id: this.todo.id },
bubbles: true,
composed: true
}))
}
private handleDelete() {
this.dispatchEvent(new CustomEvent('delete', {
detail: { id: this.todo.id },
bubbles: true,
composed: true
}))
}
render() {
return (
<div>
<input
type="checkbox"
checked={as.prop(this.todo.completed)}
onchange={() => this.handleToggle()}
/>
<span>{this.todo.text}</span>
<button onclick={() => this.handleDelete()}>Delete</button>
</div>
)
}
}
// Usage - now you get IntelliSense for ontoggle and ondelete!
<todo-item
todo={as.prop(todo)}
ontoggle={(e) => handleToggle(e.detail.id)}
ondelete={(e) => handleDelete(e.detail.id)}
/>TIP
By using declare for event handler properties, you provide IntelliSense without adding runtime overhead. These are TypeScript-only declarations that help with autocomplete and type checking.
Generic Custom Element with Custom Events
import { LitElement } from 'lit'
import { customElement, property } from 'lit/decorators.js'
interface Item {
id: string
name: string
}
@customElement('item-selector')
class ItemSelector<T extends Item> extends LitElement {
@property({ type: Array }) items: T[] = []
// Declare generic event handlers
declare onselect: ((e: CustomEvent<T>) => void) | undefined
declare onremove: ((e: CustomEvent<T>) => void) | undefined
private selectItem(item: T) {
this.dispatchEvent(new CustomEvent('select', {
detail: item,
bubbles: true,
composed: true
}))
}
private removeItem(item: T) {
this.dispatchEvent(new CustomEvent('remove', {
detail: item,
bubbles: true,
composed: true
}))
}
render() {
return (
<ul>
{this.items.map(item => (
<li key={item.id}>
<span onclick={() => this.selectItem(item)}>{item.name}</span>
<button onclick={() => this.removeItem(item)}>×</button>
</li>
))}
</ul>
)
}
}
// Usage
<item-selector
items={as.prop(items)}
onselect={(e) => console.log('Selected:', e.detail)}
onremove={(e) => console.log('Removed:', e.detail)}
/>Reactive Properties
Use @property for public reactive properties and @state for internal state:
import { LitElement, css } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
@customElement('search-box')
class SearchBox extends LitElement {
@property({ type: String }) placeholder = 'Search...'
@state() private value = ''
@state() private results: string[] = []
static styles = css`
input {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
`
private handleInput(e: Event) {
this.value = (e.target as HTMLInputElement).value
// Simulate search
this.results = this.performSearch(this.value)
}
private performSearch(query: string): string[] {
// Search logic here
return []
}
render() {
return (
<div>
<input
type="text"
placeholder={this.placeholder}
value={this.value}
oninput={(e) => this.handleInput(e)}
/>
<ul>
{this.results.map(result => (
<li key={result}>{result}</li>
))}
</ul>
</div>
)
}
}
// Usage
<search-box placeholder="Search users..." />When to Use Custom Elements
Use custom elements when:
- You need reactive properties that trigger re-renders
- You want lifecycle methods (connectedCallback, disconnectedCallback, etc.)
- You need encapsulated state and styles
- You're building reusable Web Components
- You need to use Lit directives and features
For simple, stateless rendering, use Functional Components.
Next Steps
- Learn about Functional Components
- Explore Bindings
- Check out Directives
- See the LitElement documentation