- 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
- 1. 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.
2. 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
3. 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 `
` ; } } globalThis . customElements . define ( "ct-component-name" , CTComponentName ) ; 4. 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.375 rem ) ) ; 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 `
;
}
}
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
;
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
`
)
}
;
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.