Lit Complete Guide | Fast Web Components Library
이 글의 핵심
Lit is a simple library for building fast, lightweight web components. Created by Google, it uses standard web platform features with reactive updates and declarative templates.
Introduction
Lit is a simple library for building fast, lightweight web components. It’s built on top of standard Web Components APIs and adds reactive properties, scoped styles, and declarative templates.
Why Lit?
Traditional Web Components:
class MyCounter extends HTMLElement {
constructor() {
super();
this.count = 0;
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<button>Count: ${this.count}</button>
`;
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.count++;
this.render();
});
}
}
With Lit:
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('my-counter')
class MyCounter extends LitElement {
@property({ type: Number }) count = 0;
render() {
return html`
<button @click=${() => this.count++}>
Count: ${this.count}
</button>
`;
}
}
Much cleaner!
1. Installation
npm install lit
Using with TypeScript
npm install lit
npm install -D @types/node
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"experimentalDecorators": true,
"useDefineForClassFields": false
}
}
2. Basic Component
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
@property() name = 'World';
render() {
return html`<p>Hello, ${this.name}!</p>`;
}
}
Usage in HTML:
<simple-greeting name="Alice"></simple-greeting>
3. Reactive Properties
@property Decorator
@customElement('user-profile')
export class UserProfile extends LitElement {
// String property
@property() name = '';
// Number property
@property({ type: Number }) age = 0;
// Boolean property (attribute: has-admin)
@property({ type: Boolean, attribute: 'has-admin' }) isAdmin = false;
// Object/Array (don't sync with attributes)
@property({ type: Object }) user = {};
render() {
return html`
<div>
<p>Name: ${this.name}</p>
<p>Age: ${this.age}</p>
<p>Admin: ${this.isAdmin}</p>
</div>
`;
}
}
@state (Internal State)
@customElement('toggle-button')
export class ToggleButton extends LitElement {
@state() private _open = false;
render() {
return html`
<button @click=${this._toggle}>
${this._open ? 'Close' : 'Open'}
</button>
${this._open ? html`<div>Content</div>` : ''}
`;
}
private _toggle() {
this._open = !this._open;
}
}
4. Templates
Basic Templating
render() {
const items = ['Apple', 'Banana', 'Orange'];
return html`
<h1>Fruits</h1>
<ul>
${items.map(item => html`<li>${item}</li>`)}
</ul>
`;
}
Conditional Rendering
render() {
return html`
${this.loading
? html`<p>Loading...</p>`
: html`<p>Data loaded!</p>`
}
`;
}
Event Listeners
@customElement('click-counter')
export class ClickCounter extends LitElement {
@state() count = 0;
render() {
return html`
<button @click=${this._increment}>
Clicked ${this.count} times
</button>
`;
}
private _increment() {
this.count++;
}
}
5. Styling
Static Styles
@customElement('styled-element')
export class StyledElement extends LitElement {
static styles = css`
:host {
display: block;
padding: 16px;
background: #f0f0f0;
}
button {
background: blue;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
}
button:hover {
background: darkblue;
}
`;
render() {
return html`<button>Styled Button</button>`;
}
}
Dynamic Styles
import { styleMap } from 'lit/directives/style-map.js';
@customElement('dynamic-styles')
export class DynamicStyles extends LitElement {
@property() color = 'blue';
render() {
const styles = { color: this.color, fontWeight: 'bold' };
return html`
<p style=${styleMap(styles)}>Dynamic color!</p>
`;
}
}
Class Maps
import { classMap } from 'lit/directives/class-map.js';
render() {
const classes = {
active: this.isActive,
disabled: this.isDisabled,
};
return html`
<div class=${classMap(classes)}>Content</div>
`;
}
6. Lifecycle Methods
@customElement('lifecycle-demo')
export class LifecycleDemo extends LitElement {
// Called when element is added to DOM
connectedCallback() {
super.connectedCallback();
console.log('Connected');
}
// Called when element is removed from DOM
disconnectedCallback() {
super.disconnectedCallback();
console.log('Disconnected');
}
// Called before update
willUpdate(changedProperties) {
console.log('Will update', changedProperties);
}
// Called after update
updated(changedProperties) {
console.log('Updated', changedProperties);
}
// Called on first update
firstUpdated(changedProperties) {
console.log('First update', changedProperties);
}
}
7. Directives
repeat
import { repeat } from 'lit/directives/repeat.js';
@property() users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
render() {
return html`
<ul>
${repeat(
this.users,
(user) => user.id,
(user) => html`<li>${user.name}</li>`
)}
</ul>
`;
}
when
import { when } from 'lit/directives/when.js';
render() {
return html`
${when(
this.loading,
() => html`<p>Loading...</p>`,
() => html`<p>Loaded!</p>`
)}
`;
}
until (Async)
import { until } from 'lit/directives/until.js';
async fetchData() {
const res = await fetch('/api/data');
return res.json();
}
render() {
return html`
${until(
this.fetchData().then(data => html`<p>${data.message}</p>`),
html`<p>Loading...</p>`
)}
`;
}
8. Real-World Example: Todo List
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
interface Todo {
id: number;
text: string;
done: boolean;
}
@customElement('todo-list')
export class TodoList extends LitElement {
static styles = css`
:host {
display: block;
max-width: 400px;
margin: 0 auto;
}
.todo-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-bottom: 1px solid #eee;
}
.done {
text-decoration: line-through;
opacity: 0.6;
}
input[type="text"] {
flex: 1;
padding: 8px;
}
`;
@state() private _todos: Todo[] = [];
@state() private _newTodo = '';
render() {
return html`
<h2>Todo List</h2>
<form @submit=${this._addTodo}>
<input
type="text"
.value=${this._newTodo}
@input=${this._handleInput}
placeholder="Add a todo..."
/>
<button type="submit">Add</button>
</form>
<ul>
${repeat(
this._todos,
(todo) => todo.id,
(todo) => html`
<li class="todo-item">
<input
type="checkbox"
.checked=${todo.done}
@change=${() => this._toggleTodo(todo.id)}
/>
<span class=${todo.done ? 'done' : ''}>
${todo.text}
</span>
<button @click=${() => this._deleteTodo(todo.id)}>
Delete
</button>
</li>
`
)}
</ul>
`;
}
private _handleInput(e: InputEvent) {
this._newTodo = (e.target as HTMLInputElement).value;
}
private _addTodo(e: Event) {
e.preventDefault();
if (!this._newTodo.trim()) return;
this._todos = [
...this._todos,
{ id: Date.now(), text: this._newTodo, done: false }
];
this._newTodo = '';
}
private _toggleTodo(id: number) {
this._todos = this._todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
);
}
private _deleteTodo(id: number) {
this._todos = this._todos.filter(todo => todo.id !== id);
}
}
9. Using with React
// React component
import { useRef, useEffect } from 'react';
import './my-element'; // Import Lit component
function App() {
const ref = useRef();
useEffect(() => {
const el = ref.current;
el.addEventListener('custom-event', (e) => {
console.log(e.detail);
});
}, []);
return <my-element ref={ref} name="React" />;
}
10. Best Practices
1. Use @state for Internal State
// Good: private state
@state() private _count = 0;
// Bad: public property for internal state
@property() count = 0;
2. Avoid this.shadowRoot Access
// Good: use @query
@query('#myButton') button!: HTMLButtonElement;
// Bad: manual DOM access
this.shadowRoot.querySelector('#myButton');
3. Use Directives for Complex Logic
// Good: use repeat directive
${repeat(items, (item) => item.id, renderItem)}
// Bad: array.map() without keys
${items.map(renderItem)}
Summary
Lit makes web components simple and powerful:
- Lightweight - only 5KB gzipped
- Standard - built on Web Components
- Fast - efficient reactive updates
- Universal - works with any framework
- TypeScript - excellent type support
Key Takeaways:
- Use
@propertyfor public API,@statefor internal - Templates with
htmltagged template - Styles with
csstagged template - Lifecycle methods for complex logic
- Works everywhere - vanilla JS, React, Vue
Next Steps:
- Learn Web Components
- Try Preact
- Build with Alpine.js
Resources: