A minimal, no-build web app kit using only Lit-based web components via importmaps
Find a file
2026-03-04 14:18:44 -08:00
components finished pruning for lit-only demo 2026-03-04 14:18:44 -08:00
index.html finished pruning for lit-only demo 2026-03-04 14:18:44 -08:00
README.md finished pruning for lit-only demo 2026-03-04 14:18:44 -08:00
tasks.md cleanup 2026-03-04 12:23:40 -08:00

Lit-Only Architecture (No Build)

A minimal, no-build web application using only Lit web components with local state management.

Note: This is the lit-only branch. For the full-featured version with Zustand state management and IndexedDB persistence, see the main branch.

For TSX/React Developers

If you're coming from React/TSX, here's how this minimal architecture maps to familiar concepts:

React/TSX Pattern This Architecture
useState Lit's static properties with state: true
Props drilling Property passing via .prop=${value}
Custom events CustomEvent with bubbles: true
JSX Lit's html tagged template literals
CSS-in-JS / CSS Modules Lit's css tagged template literals
React Router Simple route state in root component
Build step (Vite/Webpack) Import maps (no build!)

Key Differences

  1. No Virtual DOM: Lit uses native Web Components with efficient DOM updates
  2. No Build Step: Import maps let you use npm packages directly from CDN
  3. Local State Only: Each component manages its own state, parent manages shared state
  4. Event-Based Communication: Child components emit events, parent handles them
  5. Tagged Templates: Instead of JSX, use html\
    ...
    ``

Architecture Overview

  • Lit-based web components - Standards-based, framework-agnostic UI
  • No-build tooling - Import maps only, no bundler required
  • Local state management - Component properties and state
  • Event-driven - CustomEvents for child-to-parent communication

Project Structure

components/
  app-root.js       ← Root component with shared state
  nav-bar.js        ← Navigation component
  page-home.js      ← Home page component
  page-items.js     ← Items page component
index.html          ← Entry point with inline importmap
README.md
tasks.md

Data Flow Pattern

Parent-to-Child (Props)

// Parent passes data down
html`<child-component .items=${this.items}></child-component>`

// Child receives via properties
static properties = {
  items: { type: Array }
}

Child-to-Parent (Events)

// Child emits event
this.dispatchEvent(new CustomEvent('add-item', {
  detail: { item: newItem },
  bubbles: true,
  composed: true
}))

// Parent listens
html`<child-component @add-item=${(e) => this.handleAdd(e.detail.item)}></child-component>`

State Management Pattern

graph TD
    A[app-root] -->|.user, .items, .route| B[nav-bar]
    A -->|.user, .items| C[page-home]
    A -->|.items| D[page-items]
    B -->|@navigate event| A
    D -->|@add-item event| A
    D -->|@remove-item event| A
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#bfb,stroke:#333,stroke-width:2px
    style D fill:#bfb,stroke:#333,stroke-width:2px

Quick Start Example

1. Define a Component

// 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; }
  `

  render() {
    return html`
      <div>
        <p>Count: ${this.count}</p>
        <button @click=${() => this.count++}>
          Increment
        </button>
      </div>
    `
  }
}

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

2. Use in HTML (no build step!)

<!DOCTYPE html>
<html>
<head>
  <script type="importmap">
  {
    "imports": {
      "lit": "https://esm.sh/lit@3"
    }
  }
  </script>
</head>
<body>
  <script type="module" src="components/counter.js"></script>
  <my-counter></my-counter>
</body>
</html>

Component Communication Patterns

Pattern 1: Shared State in Root

class AppRoot extends LitElement {
  static properties = {
    items: { type: Array, state: true }
  }

  constructor() {
    super()
    this.items = []
  }

  addItem(item) {
    this.items = [...this.items, item]
  }

  render() {
    return html`
      <child-component 
        .items=${this.items}
        @add=${(e) => this.addItem(e.detail.item)}
      ></child-component>
    `
  }
}

Pattern 2: Local State in Component

class MyComponent extends LitElement {
  static properties = {
    _draft: { type: String, state: true }
  }

  constructor() {
    super()
    this._draft = ''
  }

  render() {
    return html`
      <input 
        .value=${this._draft}
        @input=${e => this._draft = e.target.value}
      />
    `
  }
}

Benefits Over React/TSX

No build step - Edit and refresh, instant feedback
Minimal dependencies - Just Lit, nothing else
Smaller bundle - No framework runtime, just standards
Better encapsulation - Shadow DOM, scoped styles
Framework agnostic - Works anywhere, even in React apps
Future-proof - Built on web standards
Easy to learn - Simple mental model, no complex state management

When to Use This vs. Full Stack

Use Lit-Only When:

  • Building small to medium apps
  • Don't need persistence
  • State is simple and hierarchical
  • Want maximum simplicity

Use Full Stack (main branch) When:

  • Need IndexedDB persistence
  • Complex state shared across many components
  • Need cross-tab synchronization
  • Want time-travel debugging
  • Building larger applications

See Also