A minimal, no-build web app kit using only Lit-based web components via importmaps
Find a file
2026-03-04 18:55:48 -08:00
components added some error handling 2026-03-04 18:55:48 -08:00
controllers attempting to resurrect full beans 2026-03-04 18:26:43 -08:00
store added some error handling 2026-03-04 18:55:48 -08:00
index.html attempting to resurrect full beans 2026-03-04 18:26:43 -08:00
README.md fixing merge conflicts 2026-03-04 18:19:47 -08:00
TASKS.md added some error handling 2026-03-04 18:55:48 -08:00

zulip

A modern, no-build web application using Lit web components with Zustand state management.

Architecture Overview

  • Lit-based web components - Standards-based, framework-agnostic UI
  • No-build tooling - Import maps only, no bundler required
  • Zustand state management - Lightweight, vanilla JS store
  • StoreController - Reactive controller that bridges Zustand and Lit

Project Structure

components/ -> standard lit components, including top-level "app-root"
controllers/ -> lit reactive controllers (such as Zustand<->Lit bridge)
store/ -> defines specific accessor sets
index.html -> entry point with inline importmap and app-root
README.md

Data Flow Diagram

sequenceDiagram
    participant User
    participant LitComponent
    participant StoreController
    participant ZustandStore

    User->>LitComponent: Click button
    LitComponent->>ZustandStore: Call action (e.g., addItem)
    ZustandStore->>ZustandStore: Update state
    ZustandStore->>StoreController: Notify subscribers
    StoreController->>LitComponent: requestUpdate()
    LitComponent->>LitComponent: Re-render
    LitComponent->>User: Show updated UI

Example

1. Define Store (Zustand Vanilla)

// store/index.js
import { createStore } from 'zustand/vanilla'

export const store = createStore((set, get) => ({
  count: 0,
  increment: () => set(s => ({ count: s.count + 1 })),
  decrement: () => set(s => ({ count: s.count - 1 })),
}))

2. Create StoreController (Bridge)

// controllers/store.js
export class StoreController {
  constructor(host, store, selector) {
    this.host = host
    this.store = store
    this.selector = selector
    host.addController(this)
  }

  hostConnected() {
    this._unsub = this.store.subscribe((state) => {
      const next = this.selector(state)
      if (next !== this.value) {
        this.value = next
        this.host.requestUpdate()
      }
    })
    this.value = this.selector(this.store.getState())
  }

  hostDisconnected() {
    this._unsub?.()
  }
}

3. Create Component (Lit + StoreController)

// components/counter.js
import { LitElement, html, css } from 'lit'

class Counter extends LitElement {
  static properties = {
    count: { type: Number, state: true }
  }

  constructor() {
    super()
    this.count = 0
  }

  static styles = css`
    button { padding: 1rem; font-size: 1.2rem; margin: 0.5rem; }
  `

  render() {
    return html`
      <div>
        <p>Count: ${this.#count.value}</p>
        <button @click=${() => store.getState().decrement()}>-</button>
        <button @click=${() => store.getState().increment()}>+</button>
      </div>
    `
  }
}

customElements.define('my-counter', Counter)

4. Instantiate in parent (such as app-root)

import counter from "./counter.js":

class AppRoot extends LitElement {
  render() {
    return html`
      <counter></counter>
    `;
  }
}

Benefits Over TSX

  1. No build step - Edit and refresh, instant feedback

  2. Smaller bundle - No framework runtime, just standards

  3. Better encapsulation - Shadow DOM, scoped styles

  4. Framework agnostic - Works anywhere, even in React apps

  5. Future-proof - Built on web standards

  6. Familiar patterns - Zustand works like Redux/Context

  7. Type-safe - Can add JSDoc or TypeScript without build step