0005. UI with Lit web components
Status
Section titled “Status”Accepted
2026-10-25
Context
Section titled “Context”The browser UI consists of the shell (toolbar, tab strip, sidebar), the overlay (menus, command palette, omnibox suggestions, prompts) and the internal pages such as settings, history and the theme editor. It has to start fast, stay small, respond to frequent state patches from the main process, and adapt to themes through design tokens (ADR 0013). The start chunk of the UI is limited to 300 KB minified and all UI code to 1 MB (ADR 0017). Every page served from app:// runs under a strict Content Security Policy with Trusted Types (ADR 0008).
The code style of the project favors small classes behind interfaces. The toolchain in ADR 0004 allows erasable syntax only: Vite 8 with Oxc cannot lower standard decorators, and Node type stripping does not support them, so decorator syntax would break either the build or the tests.
Decision
Section titled “Decision”The UI is built from class-based web components using Lit 3, without decorators.
- Reactive properties are declared with
static propertiesand typed withdeclarefields; there is no@customElement,@propertyor@state. - Elements are registered explicitly with
customElements.define. - Trivial leaf elements without reactive state are plain
HTMLElementsubclasses. - Shared base classes, design tokens and controls live in
ui/ui-kit; the shell, overlay and pages compose them. - Styles use Shadow DOM and read colors, spacing and motion only from CSS custom properties defined by the design tokens.
- Feature modules contribute UI through their own lazily loaded chunks (ADR 0010).
import { LitElement, html } from 'lit';
export class TabItem extends LitElement { static override properties = { title: { type: String }, active: { type: Boolean, reflect: true }, };
declare title: string; declare active: boolean;
constructor() { super(); this.title = ''; this.active = false; }
override render() { return html`<span class="title">${this.title}</span>`; }}
customElements.define('astela-tab-item', TabItem);Consequences
Section titled “Consequences”Positive:
- Lit adds about 6 KB gzipped, which leaves almost the whole UI budget to Astela’s own code.
- Components are standard custom elements, so they work without a framework runtime and are easy to test in a real browser engine with Vitest Browser Mode and axe-core (ADR 0022).
- Lit’s templates render without
eval, which fits the strict CSP and Trusted Types of the internal pages. - The class-based model matches the rest of the codebase and the dependency injection style.
Negative:
- Without decorators, components are slightly more verbose: each reactive property is listed once in
static propertiesand once as adeclarefield. - Most Lit examples and documentation use decorators and have to be translated.
- Shadow DOM requires care for focus handling, accessibility labels across shadow roots and global keyboard navigation.
Alternatives considered
Section titled “Alternatives considered”- Lit with decorators: the most common Lit style, but the toolchain cannot compile standard decorators, and legacy decorators violate the erasable syntax rule.
- React or Preact: larger runtime (React) or a virtual DOM and a JSX transform in the build, and a function-component model that differs from the rest of the codebase.
- Svelte or Solid: small output, but a framework compiler with its own syntax in the build chain.
- Stencil: relies on decorators and its own compiler.
- Plain custom elements everywhere: no dependency, but templating and reactive updates would be reinvented; plain
HTMLElementstays the choice for trivial leaves only.