LitElement Advanced — Directives, Slots, and Performance Optimization
In this tutorial, you will learn about LitElement Advanced. We cover key concepts, practical examples, and best practices to help you master this topic.
LitElement's advanced features include built-in directives for efficient rendering, composition with named slots, and performance optimization through update lifecycle control.
What You'll Learn
- lit-html directives (repeat, classMap, styleMap, ifDefined)
- Advanced slot composition
- Shadow Dom configuration
- Update cycle optimization
- Template partials and composition
Why It Matters
Directives optimize rendering by tracking individual items and conditionally applying styles. Understanding the update cycle prevents performance bottlenecks in complex components.
Real-World Use
A data table with 1000+ rows using repeat directive, virtual scrolling via directives, and complex slot-based layout composition.
Advanced Architecture
flowchart TD
A[Advanced] --> B[Directives]
A --> C[Composition]
A --> D[Optimization]
B --> E[repeat]
B --> F[classMap]
B --> G[styleMap]
C --> H[Named Slots]
C --> I[Slot Fallback]
D --> J[shouldUpdate]
D --> K[updateComplete]
D --> L[Async Rendering]
style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
repeat Directive
import { LitElement, html, css } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
class SortableList extends LitElement {
static properties = { items: { type: Array }, sortKey: { type: String } };
constructor() {
super();
this.items = [
{ id: 'a', name: 'Alpha', value: 30 },
{ id: 'b', name: 'Beta', value: 10 },
{ id: 'c', name: 'Gamma', value: 20 }
];
this.sortKey = 'name';
}
get _sorted() {
return [...this.items].sort((a, b) =>
a[this.sortKey] > b[this.sortKey] ? 1 : -1
);
}
_remove(id) { this.items = this.items.filter(i => i.id !== id); }
render() {
return html`
<button @click=${() => this.sortKey = 'name'}>Sort by Name</button>
<button @click=${() => this.sortKey = 'value'}>Sort by Value</button>
${repeat(this._sorted, item => item.id, item => html`
<div class="item">
<span>${item.name}: ${item.value}</span>
<button @click=${() => this._remove(item.id)}>X</button>
</div>
`)}
`;
}
}
customElements.define('sortable-list', SortableList);
Expected output: repeat tracks items by key function. Removing items preserves DOM state of unchanged items.
classMap and styleMap
import { LitElement, html, css } from 'lit';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
class AlertBanner extends LitElement {
static styles = css`
:host { display: block; }
.alert { padding: 12px 16px; border-radius: 4px; display: flex; align-items: center; gap: 8px; }
.info { background: #e3f2fd; border-left: 4px solid #1565c0; }
.success { background: #e8f5e9; border-left: 4px solid #2e7d32; }
.warning { background: #fff3e0; border-left: 4px solid #e65100; }
.error { background: #ffebee; border-left: 4px solid #c62828; }
.dismissed { display: none; }
`;
static properties = {
type: { type: String },
dismissible: { type: Boolean },
dismissed: { state: true }
};
constructor() { super(); this.type = 'info'; this.dismissible = false; this.dismissed = false; }
get _classes() {
return {
alert: true,
[this.type]: true,
dismissed: this.dismissed
};
}
get _styles() {
return {
maxWidth: ifDefined(this.dismissible ? '400px' : undefined),
opacity: this.dismissed ? '0' : '1'
};
}
render() {
return html`
<div class=${classMap(this._classes)} style=${styleMap(this._styles)}>
<slot></slot>
${this.dismissible ? html`
<button @click=${() => this.dismissed = true}>Dismiss</button>
` : ''}
</div>
`;
}
}
customElements.define('alert-banner', AlertBanner);
Expected output: classMap conditionally applies CSS classes. styleMap applies dynamic inline styles. ifDefined omits undefined values.
Portal Pattern with Teleport
import { LitElement, html, css } from 'lit';
class Modal extends LitElement {
static styles = css`
:host { display: contents; }
.overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;
z-index: 1000;
}
.modal {
background: white; border-radius: 8px; padding: 24px; min-width: 320px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
`;
static properties = { open: { type: Boolean, reflect: true } };
render() {
if (!this.open) return html``;
return html`
<div class="overlay" @click=${e => { if (e.target === e.currentTarget) this.open = false; }}>
<div class="modal">
<slot></slot>
</div>
</div>
`;
}
createRenderRoot() {
return this;
}
}
customElements.define('my-modal', Modal);
Expected output: createRenderRoot returning this renders without Shadow DOM. The modal positions fixed relative to document body.
Slot Composition with Named Slots
import { LitElement, html, css } from 'lit';
class DataTable extends LitElement {
static styles = css`
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 12px; border-bottom: 1px solid #e0e0e0; text-align: left; }
th { background: #f5f5f5; font-weight: 600; }
.empty { text-align: center; padding: 48px; color: #999; }
::slotted(tr[selected]) { background: #e8eaf6; }
::slotted(.highlight) { font-weight: bold; }
`;
get _columns() {
return Array.from(this.querySelectorAll('[slot="header"]'));
}
render() {
return html`
<table>
<thead><tr><slot name="header"></slot></tr></thead>
<tbody><slot name="body">
<tr><td colspan="99" class="empty">No data</td></tr>
</slot></tbody>
</table>
`;
}
}
customElements.define('data-table', DataTable);
<data-table>
<th slot="header">Name</th>
<th slot="header">Email</th>
<tr slot="body"><td>Alice</td><td>alice@example.com</td></tr>
<tr slot="body"><td>Bob</td><td>bob@example.com</td></tr>
</data-table>
Expected output: Named slots for header and body. Fallback content for empty body. ::slotted styles apply to projected content.
Update Optimization
import { LitElement, html, css } from 'lit';
class OptimizedPanel extends LitElement {
static properties = {
visible: { type: Boolean },
config: { type: Object },
filter: { type: String }
};
constructor() {
super();
this.visible = true;
this.config = {};
this.filter = '';
this._data = [];
this._cache = new Map();
}
shouldUpdate(changedProperties) {
if (!this.visible && !changedProperties.has('visible')) return false;
if (changedProperties.has('filter') || changedProperties.has('config')) {
this._fetchData();
}
return true;
}
updated(changedProperties) {
if (changedProperties.has('visible') && this.visible) {
this._onShow();
}
}
get updateComplete() {
return super.updateComplete.then(() => {
this.shadowRoot.querySelector('.container')?.classList.add('ready');
});
}
_onShow() { /* analytics or lazy load */ }
_fetchData() { /* fetch based on filter + config */ }
render() {
return html`<div class="container">${this.visible ? html`<slot></slot>` : ''}</div>`;
}
}
customElements.define('optimized-panel', OptimizedPanel);
Expected output: shouldUpdate prevents unnecessary renders. updateComplete returns a promise resolving after render.
Template Partials
import { LitElement, html, css } from 'lit';
class Wizard extends LitElement {
static properties = { step: { type: Number } };
constructor() { super(); this.step = 1; }
_header() {
return html`<header><h1>Step ${this.step} of 3</h1></header>`;
}
_navigation() {
return html`
<nav>
<button ?disabled=${this.step === 1} @click=${() => this.step--}>Back</button>
<button @click=${() => this.step < 3 ? this.step++ : this._finish()}>
${this.step === 3 ? 'Finish' : 'Next'}
</button>
</nav>
`;
}
_stepContent() {
switch (this.step) {
case 1: return html`<div class="step"><h2>Personal Info</h2><slot name="step1"></slot></div>`;
case 2: return html`<div class="step"><h2>Preferences</h2><slot name="step2"></slot></div>`;
case 3: return html`<div class="step"><h2>Review</h2><slot name="step3"></slot></div>`;
default: return html``;
}
}
_finish() {
this.dispatchEvent(new CustomEvent('wizard-complete', {
bubbles: true, composed: true
}));
}
render() {
return html`
${this._header()}
${this._stepContent()}
${this._navigation()}
`;
}
}
customElements.define('my-wizard', Wizard);
Expected output: Template partials (methods returning html) improve readability. Each step renders its slot content.
Common Mistakes
Not providing a key function to repeat - Without keys, repeat reverts to array.map behavior.
Mutating class/style objects - classMap and styleMap need new objects per render.
Shadow DOM for fixed/positioned elements - Use createRenderRoot for portal-like components.
Overusing createRenderRoot - Only opt out of shadow DOM when needed (modals, toasts).
Forgetting updateComplete for async operations - Use updateComplete for timing after render.
Practice Questions
- How does the repeat directive differ from Array.map in render?
- How do you conditionally apply CSS classes in lit-html?
- How do you render a component without Shadow DOM?
- How does shouldUpdate prevent unnecessary renders?
- What is the updateComplete promise used for?
Challenge: Build a virtual scrolling list with repeat directive, item recycling, dynamic height, intersection Observer for Lazy Loading, and scroll position restoration.
FAQ
Mini Project
Build a complex dashboard with: virtual scrolling data table with repeat directive, filter/sort with classMap/styleMap, modal dialog with createRenderRoot, wizard for configuration, and aggregated layout with slot composition.
What's Next
Advanced patterns optimize components. Learn how Polymer Routing manages application navigation with page.js.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro