Lit Component Development for Common UI
This skill provides guidance for developing Lit web components within the Common UI v2 component library (packages/ui/src/v2 ).
When to Use This Skill
Use this skill when:
-
Creating new ct- prefixed components in the UI package
-
Modifying existing Common UI v2 components
-
Implementing theme-aware components
-
Integrating components with Cell abstractions from the runtime
-
Building reactive components for pattern UIs
-
Debugging component lifecycle or reactivity issues
Core Philosophy
Common UI is inspired by SwiftUI and emphasizes:
-
Default Configuration Works: Components should work together with minimal configuration
-
Composition Over Control: Emphasize composing components rather than granular styling
-
Adaptive to User Preferences: Respect system preferences and theme settings (theme is ambient context, not explicit props)
-
Reactive Binding Model: Integration with FRP-style Cell abstractions from the runtime
-
Progressive Enhancement: Components work with plain values but enhance with Cells for reactivity
-
Separation of Concerns: Presentation components, theme-aware inputs, Cell-aware state, runtime-integrated operations
Quick Start Pattern
- Choose Component Category
Identify which category the component falls into:
-
Layout: Arranges other components (vstack, hstack, screen)
-
Visual: Displays styled content (separator, skeleton, label)
-
Input: Captures user interaction (button, input, checkbox)
-
Complex/Integrated: Deep runtime integration with Cells (render, code-editor, outliner)
Complexity spectrum: Components range from pure presentation (no runtime) to deeply integrated (Cell operations, pattern execution, backlink resolution). Choose the simplest pattern that meets requirements.
See references/component-patterns.md for detailed patterns for each category and references/advanced-patterns.md for complex integration patterns.
- Create Component Files
Create the component directory structure:
packages/ui/src/v2/components/ct-component-name/ ├── ct-component-name.ts # Component implementation ├── index.ts # Export and registration └── styles.ts # Optional: for complex components
- Implement Component
Basic template:
import { css, html } from "lit"; import { BaseElement } from "../../core/base-element.ts";
export class CTComponentName extends BaseElement { static override styles = [ BaseElement.baseStyles, css` :host { display: block; box-sizing: border-box; }
*,
*::before,
*::after {
box-sizing: inherit;
}
`,
];
static override properties = { // Define reactive properties };
constructor() { super(); // Set defaults }
override render() {
return html<!-- component template -->;
}
}
globalThis.customElements.define("ct-component-name", CTComponentName);
- Create Index File
import { CTComponentName } from "./ct-component-name.ts";
if (!customElements.get("ct-component-name")) { customElements.define("ct-component-name", CTComponentName); }
export { CTComponentName }; export type { /* exported types */ };
Theme Integration
For components that need to consume theme (input and complex components):
import { consume } from "@lit/context"; import { property } from "lit/decorators.js"; import { applyThemeToElement, type CTTheme, defaultTheme, themeContext, } from "../theme-context.ts";
export class MyComponent extends BaseElement { @consume({ context: themeContext, subscribe: true }) @property({ attribute: false }) declare theme?: CTTheme;
override firstUpdated(changed: Map<string | number | symbol, unknown>) { super.firstUpdated(changed); this._updateThemeProperties(); }
override updated(changed: Map<string | number | symbol, unknown>) { super.updated(changed); if (changed.has("theme")) { this._updateThemeProperties(); } }
private _updateThemeProperties() { const currentTheme = this.theme || defaultTheme; applyThemeToElement(this, currentTheme); } }
Then use theme CSS variables with fallbacks:
.button { background-color: var( --ct-theme-color-primary, var(--ct-colors-primary-500, #3b82f6) ); border-radius: var( --ct-theme-border-radius, var(--ct-border-radius-md, 0.375rem) ); font-family: var(--ct-theme-font-family, inherit); }
Complete theme reference: See references/theme-system.md for all available CSS variables and helper functions.
Cell Integration
For components that work with reactive runtime data:
import { property } from "lit/decorators.js"; import type { Cell } from "@commontools/runner"; import { isCell } from "@commontools/runner";
export class MyComponent extends BaseElement { @property({ attribute: false }) declare cell: Cell<MyDataType>;
private _unsubscribe: (() => void) | null = null;
override updated(changedProperties: Map<string, any>) { super.updated(changedProperties);
if (changedProperties.has("cell")) {
// Clean up previous subscription
if (this._unsubscribe) {
this._unsubscribe();
this._unsubscribe = null;
}
// Subscribe to new Cell
if (this.cell && isCell(this.cell)) {
this._unsubscribe = this.cell.sink(() => {
this.requestUpdate();
});
}
}
}
override disconnectedCallback() { super.disconnectedCallback(); if (this._unsubscribe) { this._unsubscribe(); this._unsubscribe = null; } }
override render() { if (!this.cell) return html``;
const value = this.cell.get();
return html`<div>${value}</div>`;
} }
Complete Cell patterns: See references/cell-integration.md for:
-
Subscription management
-
Nested property access with .key()
-
Array cell manipulation
-
Transaction-based mutations
-
Finding cells by equality
Reactive Controllers
For reusable component behaviors, use reactive controllers. Example: InputTimingController for debouncing/throttling:
import { InputTimingController } from "../../core/input-timing-controller.ts";
export class CTInput extends BaseElement { @property() timingStrategy: "immediate" | "debounce" | "throttle" | "blur" = "debounce";
@property() timingDelay: number = 500;
private inputTiming = new InputTimingController(this, { strategy: this.timingStrategy, delay: this.timingDelay, });
private handleInput(event: Event) { const value = (event.target as HTMLInputElement).value;
this.inputTiming.schedule(() => {
this.emit("ct-change", { value });
});
} }
Common Patterns
Event Emission
Use the emit() helper from BaseElement :
private handleChange(newValue: string) { this.emit("ct-change", { value: newValue }); }
Events are automatically bubbles: true and composed: true .
Dynamic Classes
Use classMap for conditional classes:
import { classMap } from "lit/directives/class-map.js";
const classes = { button: true, [this.variant]: true, disabled: this.disabled, };
return html<button class="${classMap(classes)}">...</button>;
List Rendering
Use repeat directive with stable keys:
import { repeat } from "lit/directives/repeat.js";
return html ${repeat( items, (item) => item.id, // stable key (item) => html<div>${item.title}</div> )};
Testing
Colocate tests with components:
// ct-button.test.ts import { describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { CTButton } from "./ct-button.ts";
describe("CTButton", () => { it("should be defined", () => { expect(CTButton).toBeDefined(); });
it("should have default properties", () => { const element = new CTButton(); expect(element.variant).toBe("primary"); }); });
Run with: deno task test (includes required flags)
Package Structure
Components are exported from @commontools/ui/v2 :
// packages/ui/src/v2/index.ts export { CTButton } from "./components/ct-button/index.ts"; export type { ButtonVariant } from "./components/ct-button/index.ts";
Reference Documentation
Load these references as needed for detailed guidance:
-
references/component-patterns.md
-
Detailed patterns for each component category, file structure, type safety, styling conventions, event handling, and lifecycle methods
-
references/theme-system.md
-
Theme philosophy, ct-theme provider, CTTheme interface, CSS variables, and theming patterns
-
references/cell-integration.md
-
Comprehensive Cell integration patterns including subscriptions, mutations, array handling, and common pitfalls
-
references/advanced-patterns.md
-
Advanced architectural patterns revealed by complex components: context provision, third-party integration, reactive controllers, path-based operations, diff-based rendering, and progressive enhancement
Key Conventions
-
Always extend BaseElement
-
Provides emit() helper and base CSS variables
-
Include box-sizing reset - Ensures consistent layout behavior
-
Use attribute: false for objects/arrays/Cells - Prevents serialization errors
-
Prefix custom events with ct-
-
Namespace convention
-
Export types separately - Use export type { ... }
-
Clean up subscriptions - Always unsubscribe in disconnectedCallback()
-
Use transactions for Cell mutations - Never mutate cells directly
-
Provide CSS variable fallbacks - Components should work without theme context
-
Document with JSDoc - Include @element , @attr , @fires , @example
-
Run tests with deno task test
-
Not plain deno test
Common Pitfalls to Avoid
-
❌ Forgetting to clean up Cell subscriptions (causes memory leaks)
-
❌ Mutating Cells without transactions (breaks reactivity)
-
❌ Using array index as key in repeat() (breaks reactivity)
-
❌ Missing box-sizing reset (causes layout issues)
-
❌ Not providing CSS variable fallbacks (breaks without theme)
-
❌ Using attribute: true for objects/arrays (serialization errors)
-
❌ Skipping super calls in lifecycle methods (breaks base functionality)
Architecture Patterns to Study
Study these components to understand architectural patterns:
Basic patterns:
-
Simple visual: ct-separator
-
Minimal component, CSS parts, ARIA
-
Layout: ct-vstack
-
Flexbox abstraction, utility classes with classMap
-
Themed input: ct-button
-
Theme consumption, event emission, variants
Advanced patterns:
-
Context provider: ct-theme
-
Ambient configuration with @provide , display: contents , reactive Cell subscriptions
-
Runtime rendering: ct-render
-
Pattern loading, UI extraction, lifecycle management
-
Third-party integration: ct-code-editor
-
CodeMirror lifecycle, Compartments, bidirectional sync, CellController
-
Tree operations: ct-outliner
-
Path-based operations, diff-based rendering, keyboard commands, MentionController
Each component reveals deeper patterns - study them not just for API but for architectural principles.