loom-ui-cli
v0.2.4
Published
Agent-native UI framework — CLI-distributed, manifest-driven, zero-dependency components
Downloads
951
Maintainers
Readme
Loom UI
Agent-native UI framework. Manifest-driven, zero-class, zero-dependency. Built for AI agents to generate, inspect, and repair — and for developers to fully own.
No classes. No build step. No runtime dependencies. Just data attributes, design tokens, and machine-readable manifests.
The CSS is the component.
The JSON manifest is the documentation.
The AI is the compiler.
The CLI is the conductor.Table of Contents
- Why Loom?
- Quick Start
- The Attribute Protocol
- Component Library
- Layout System
- Design Token System
- Theme System
- Loom Core — Reactive Engine
- The Manifest System
- JavaScript Controllers
- CLI Reference
- CSS Bundle
- Audit and Repair
- Scaffolding and Code Generation
- Data-Driven Rendering
- AI Agent Integration
- CSS Conventions
- Project Structure
- Development
- License
Why Loom?
Traditional UI frameworks use class names: .btn, .btn-primary, .card-header. This creates naming collisions, specificity wars, and markup that no machine can reliably parse. A class name is ambiguous — is .active a state, a variant, or a layout helper?
Loom replaces all of it with a five-attribute protocol where every attribute has a single, unambiguous purpose:
<!-- Traditional -->
<button class="btn btn-primary btn-lg is-loading">Save</button>
<!-- Loom -->
<button data-ui="button" data-variant="primary" data-size="lg" data-state="loading">Save</button>Every component is machine-readable. Every variant is auditable. Every state change is traceable. The CSS targets data attributes — never classes.
This makes Loom agent-native: AI coding agents can read manifests, generate valid markup, audit it against contracts, and auto-repair violations. But it's equally good for developers — you get a complete component library, a dev server, CSS bundling, and full ownership of every file.
What Loom Is NOT
- Not a JavaScript framework (no virtual DOM, no JSX, no compile step)
- Not a utility-first CSS library (not Tailwind)
- Not a package you import at runtime (no
node_modulesdependency) - Not a design system only for humans to browse — it's a design system for agents to parse and developers to own
Quick Start
Prerequisites
- Bun runtime
Install and Initialize
# Install globally (or use npx)
npm install -g loom-ui-cli
# Initialize a new project
loom init
# Add components
loom add button input card dialog tabs stack grid surface
# Start dev server
loom devWhat loom init Creates
your-project/
├── ui/
│ ├── tokens/ Design tokens (CSS custom properties)
│ ├── base/ CSS reset and prose styles
│ ├── core/ Reactive engine + recipe controllers
│ ├── primitives/ (empty — add components with `loom add`)
│ ├── recipes/ (empty — add components with `loom add`)
│ ├── patterns/ (empty — add components with `loom add`)
│ └── loom.bundle.css Single CSS bundle (auto-generated)
├── loom.config.json Project configuration
└── .loom/
└── context.json AI agent context (auto-generated)Use in HTML
After adding components, include one CSS file and the reactive engine:
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="ui/loom.bundle.css">
<script src="ui/core/loom-core.js" defer></script>
</head>
<body>
<div data-ui="surface" data-variant="flat" data-size="lg">
<div data-ui="stack" data-gap="6">
<h1>Hello Loom</h1>
<button data-ui="button" data-variant="primary">Get Started</button>
</div>
</div>
</body>
</html>One <link> tag. One <script> tag. That's the entire framework inclusion.
The Attribute Protocol
Five data attributes form the stable DOM contract between HTML, CSS, JavaScript, and AI agents.
| Attribute | Purpose | Set By | Example |
|-----------|---------|--------|---------|
| data-ui | Component identity | Markup | data-ui="button" |
| data-part | Named slot within parent | Markup | data-part="trigger" |
| data-state | Runtime state | JavaScript only | data-state="open" |
| data-variant | Visual variant | Markup (set once) | data-variant="primary" |
| data-size | Size variant | Markup (set once) | data-size="lg" |
Rules
data-uigoes on the root element of every component instance.data-partidentifies child slots within a parent component.data-stateis the only attribute JavaScript controllers modify. CSS reacts to it.data-variantanddata-sizeare set in markup and rarely change at runtime.- Components never use CSS class names. State lives in
data-state. Identity lives indata-ui. - Standard HTML attributes (
role,aria-*,hidden,disabled) work alongside data attributes.
How CSS Targets the Protocol
[data-ui="button"] { } /* base styles */
[data-ui="button"][data-variant="primary"] { } /* variant */
[data-ui="button"][data-size="lg"] { } /* size */
[data-ui="dialog"][data-state="open"] [data-part="panel"] { } /* state + part */
[data-ui="card"] [data-part="header"] { } /* scoped part */No specificity wars. No naming conventions to memorize. The selector is the documentation.
Component Library
Loom ships 53 components across three layers, from simple CSS-only primitives to full interactive recipes and page-level patterns.
Primitives (30 components) — CSS Only
Pure CSS components. No JavaScript required. Drop in the HTML and it works.
| Component | Description | Variants |
|-----------|-------------|----------|
| button | Action trigger | primary, secondary, destructive, ghost, outline, link + sm/md/lg |
| input | Text input field | error, disabled states |
| textarea | Multi-line text | error, disabled states |
| select | Native select dropdown | error, disabled states |
| checkbox | Form checkbox | checked, indeterminate |
| radio | Radio button | checked state |
| switch | Toggle switch | checked state |
| label | Form label | required indicator |
| card | Container with slots | header, body, footer parts |
| badge | Status indicator | primary, secondary, success, warning, destructive |
| avatar | Profile image/initials | sm, md, lg sizes |
| separator | Horizontal/vertical divider | horizontal, vertical + solid, dashed, dotted, thick styles |
| spinner | Loading animation | sm, md, lg sizes |
| kbd | Keyboard key display | — |
| progress | Progress bar | determinate, indeterminate |
| stepper | Multi-step indicator | active, completed states |
| empty-state | Placeholder for empty content | — |
| nav | Navigation container | — |
| text | Text with semantic styles | muted, sm/lg/xl sizes |
| stack | Flexbox layout | vertical (default), horizontal |
| grid | CSS Grid layout | 1–12 columns, responsive |
| surface | Container with elevation | flat, raised, overlay |
| callout | Notice/warning box | info, warning, destructive, success, muted |
| description-list | Styled dl/dt/dd pairs | vertical, horizontal + sm/md/lg |
| field-group | Form field wrapper (label + input + error) | vertical, horizontal + error/valid states |
| image | Responsive image with caption | responsive, thumbnail, cover, contain + xs–full sizes |
| key-value | Labeled data pair | horizontal, vertical, inline + grid columns |
| page-break | Print page break | after (default), before |
| signature | Signing line for documents | sm/md/lg + left/center/right alignment |
| stat | Metric display with trend | default, card + up/down/neutral trend |
Recipes (16 components) — CSS + JavaScript
Interactive components with JavaScript controllers. Auto-initialize when loom-core.js is loaded.
| Component | Description | Key Features |
|-----------|-------------|-------------|
| dialog | Modal dialog | Focus trap, escape-to-close, ARIA modal |
| drawer | Side panel | Slides from left/right, overlay |
| sheet | Full/partial overlay panel | Bottom sheet pattern |
| dropdown | Action menu | Keyboard navigation, click-outside-close |
| popover | Floating content | Positioned relative to trigger |
| tooltip | Hover information | Delay, positioning |
| tabs | Tab panel switcher | Arrow key navigation, ARIA tabs |
| accordion | Expandable sections | Single/multi open modes |
| combobox | Searchable select | Filtering, keyboard selection |
| select-custom | Custom-styled select | Full keyboard support |
| command-palette | Command menu (Cmd+K) | Fuzzy search, sections |
| table | Data table | Sortable, row selection, footer, alignment, grouped rows, print compact |
| pagination | Page navigation | Previous/next, page numbers |
| toast | Notification messages | Auto-dismiss, stacking |
| date-picker | Calendar date selection | Month navigation, range selection |
| qr-code | SVG QR code generator | sm/md/lg sizes, error correction levels (L/M/Q/H) |
Patterns (7 compositions) — No Custom JS
Pre-built page-level compositions that combine primitives and recipes.
| Pattern | Composes |
|---------|----------|
| auth-form | card, input, button, separator, label |
| dashboard-shell | card, grid, avatar, dropdown, button, nav |
| settings-page | tabs, card, input, switch, button |
| crud-table | table, button, dropdown, dialog, pagination |
| empty-state | button, card |
| search-results | grid, input, button, badge, pagination |
| document | Full-page print/PDF container (invoice, form, report) with A4/letter formats |
Layout System
Three layout primitives replace CSS utility classes for page structure. No .container, .row, .col-6 — just components.
Stack — Flex Container
<!-- Vertical stack with gap -->
<div data-ui="stack" data-gap="4">
<p>First</p>
<p>Second</p>
</div>
<!-- Horizontal stack, centered -->
<div data-ui="stack" data-variant="horizontal" data-gap="3" data-align="center">
<button data-ui="button">Cancel</button>
<button data-ui="button" data-variant="primary">Save</button>
</div>| Attribute | Values |
|-----------|--------|
| data-gap | 0, 1, 2, 3, 4, 6, 8 |
| data-align | start, center, end, stretch |
| data-justify | start, center, end, between |
| data-variant | horizontal |
| data-wrap | (boolean attribute) |
Grid — Column Layout
<!-- Three-column grid, responsive -->
<div data-ui="grid" data-cols="3" data-gap="4">
<div data-ui="card">...</div>
<div data-ui="card">...</div>
<div data-ui="card">...</div>
</div>| Attribute | Values |
|-----------|--------|
| data-cols | 1, 2, 3, 4, 6, 12 |
| data-cols-sm | Override columns below 640px |
| data-gap | 2, 4, 6, 8 |
Auto-stacks to 1 column on screens under 640px by default.
Surface — Container
<div data-ui="surface" data-variant="raised" data-size="lg">
Content with padding and shadow
</div>| Attribute | Values |
|-----------|--------|
| data-variant | flat, raised, overlay |
| data-size | sm, md, lg |
| data-max | sm, md, lg, xl (max-width) |
Composing Layouts
<div data-ui="surface" data-size="lg" data-max="xl" style="margin: 0 auto">
<div data-ui="stack" data-gap="8">
<!-- Header -->
<div data-ui="stack" data-variant="horizontal" data-align="center" data-justify="between">
<h1>Dashboard</h1>
<button data-ui="button" data-variant="primary">New Item</button>
</div>
<!-- Card grid -->
<div data-ui="grid" data-cols="3" data-gap="4" data-cols-sm="1">
<div data-ui="card">
<div data-part="body">Card one</div>
</div>
<div data-ui="card">
<div data-part="body">Card two</div>
</div>
<div data-ui="card">
<div data-part="body">Card three</div>
</div>
</div>
<!-- Actions -->
<div data-ui="stack" data-variant="horizontal" data-gap="3" data-align="center">
<button data-ui="button" data-variant="outline">Cancel</button>
<button data-ui="button" data-variant="primary">Save</button>
</div>
</div>
</div>Design Token System
All styling uses CSS custom properties organized in three layers. Components reference only semantic tokens — never raw palette values.
Layer 1: Palette (raw values)
Never referenced by components directly. These define the color space:
--palette-indigo-500: oklch(0.55 0.22 264);
--palette-red-500: oklch(0.55 0.22 27);
--palette-gray-200: oklch(0.91 0.004 264);Layer 2: Semantic (what components use)
Purpose-based tokens that map to palette values. Themes override these.
/* Surfaces */
--color-bg --color-bg-subtle --color-bg-muted
--color-fg --color-fg-muted --color-fg-subtle
/* Interactive */
--color-primary --color-primary-hover --color-primary-fg
--color-secondary --color-destructive --color-success
--color-warning --color-info
/* Borders */
--color-border --color-border-strong --color-ringLayer 3: Aliases (component-specific)
Optional overrides for fine-tuning individual components:
--button-radius --button-height-md
--card-shadow --card-padding
--input-radius --input-height
--dialog-radius --dialog-shadowOther Token Categories
| File | Key Tokens |
|------|------------|
| spacing.css | --space-0 through --space-24 (4px base scale) |
| typography.css | --font-sans, --font-mono, --text-xs through --text-4xl, --weight-*, --leading-* |
| effects.css | --radius-sm through --radius-2xl, --shadow-xs through --shadow-xl, --z-* |
| motion.css | --ease-default, --ease-in-out, --duration-fast (150ms), --duration-normal (250ms), --duration-slow (350ms) |
| document.css | --page-format, --page-margin, --doc-font, --doc-heading-size, --doc-table-*, --doc-signature-*, --doc-max-width |
| doc-aliases.css | --kv-*, --callout-*, --image-*, --field-*, --page-break-*, --stat-* (component-level document aliases) |
Theme System
Themes override Layer 2 semantic tokens. Four built-in themes ship with Loom:
| Theme | Description |
|-------|-------------|
| default | Clean modern. Light mode + dark mode via [data-theme="dark"] |
| midnight | Deep navy with cyan accents. High-contrast dark theme |
| paper | Warm cream backgrounds, earthy brown accents. Overrides document tokens for warmth |
| brutalist | Black and white. No shadows. No border radius |
| document | Clean, professional, PDF-optimized. No shadows, no radius, pt-based sizes |
Using Themes
<!-- Light mode (default) -->
<html data-theme="light">
<!-- Dark mode -->
<html data-theme="dark">
<!-- Auto (follows system preference) -->
<html data-theme="auto">Managing Themes via CLI
# Switch active theme
loom theme set midnight
# Create a custom theme (generates a CSS file with all tokens commented out)
loom theme create my-brand
# List available themes
loom theme listCustom themes are scaffold files with every semantic token as a commented-out override. Uncomment and modify what you need.
Loom Core — Reactive Engine
loom-core.js is a zero-dependency reactive engine (~47KB min, ~12KB gzip). Drop it in with a single script tag — no build step required.
<script src="ui/core/loom-core.js" defer></script>It provides Alpine.js-style reactive directives, automatic recipe controller initialization, and a global store.
Directives
| Directive | Shorthand | Purpose |
|-----------|-----------|---------|
| l-data | — | Create reactive scope with initial state |
| l-text | — | Set text content reactively |
| l-html | — | Set inner HTML reactively |
| l-bind:attr | :attr | Bind element attributes |
| l-on:event | @event | Event listeners |
| l-model | — | Two-way form binding |
| l-show | — | Toggle visibility (with transitions) |
| l-if | — | Conditional rendering (on <template>) |
| l-for | — | List rendering (on <template>) |
| l-ref | — | Named element reference |
| l-init | — | Run code once on initialization |
| l-effect | — | Tracked reactive side effect |
| l-cloak | — | Hide element until Loom initializes |
| l-source:name | — | Declarative REST data binding (injects array + CRUD controller) |
Event Modifiers
@click.prevent, @submit.stop, @keydown.enter, @click.once, @input.debounce.300ms, @resize.throttle.100ms, @click.self
Model Modifiers
l-model.number, l-model.trim, l-model.lazy, l-model.debounce.300ms
Magic Properties
| Property | Description |
|----------|-------------|
| $el | Current element |
| $refs | Named element references |
| $store | Global reactive store |
| $state | Sync reactive state with data-state |
| $variant | Sync with data-variant |
| $ui | Access recipe controller API |
| $dispatch | Dispatch custom events |
| $nextTick | Run after DOM update |
| $watch | Watch reactive value changes |
| $id | Generate unique IDs |
Examples
<!-- Counter -->
<div l-data="{ count: 0 }">
<span l-text="count"></span>
<button data-ui="button" @click="count++">+1</button>
</div>
<!-- Two-way binding -->
<div l-data="{ name: '' }">
<input data-ui="input" l-model="name" placeholder="Your name">
<p>Hello, <span l-text="name || 'stranger'"></span>!</p>
</div>
<!-- Conditional list -->
<div l-data="{ items: ['Apple', 'Banana', 'Cherry'], show: true }">
<button data-ui="button" @click="show = !show">Toggle</button>
<template l-if="show">
<div data-ui="stack" data-gap="2">
<template l-for="item in items">
<span data-ui="badge" l-text="item"></span>
</template>
</div>
</template>
</div>
<!-- Global store -->
<script>
Loom.store('app', { theme: 'light', user: 'Agent' });
</script>
<div l-data="{}">
<span l-text="$store.app.user"></span>
<button data-ui="button" @click="$store.app.theme = $store.app.theme === 'light' ? 'dark' : 'light'">
Toggle Theme
</button>
</div>The Manifest System
Every component ships with a .manifest.json — a machine-readable contract that drives audit, repair, AI context generation, and code generation.
Manifest Structure
{
"name": "dialog",
"version": "1.0.0",
"kind": "recipe",
"category": "overlay",
"description": "Modal dialog with focus trap and escape-to-close",
"anatomy": {
"tag": "div",
"selector": "[data-ui='dialog']",
"content_model": "slots"
},
"slots": {
"trigger": { "selector": "[data-part='trigger']", "required": true },
"overlay": { "selector": "[data-part='overlay']", "required": true },
"panel": { "selector": "[data-part='panel']", "required": true },
"title": { "selector": "[data-part='title']", "required": true },
"close": { "selector": "[data-part='close']", "required": true },
"body": { "selector": "[data-part='body']", "required": false }
},
"variants": {},
"states": {
"open": { "attr": "data-state=\"open\"" }
},
"a11y": {
"role": "dialog",
"aria-modal": true,
"focus_trap": true,
"escape_closes": true,
"keyboard": { "Escape": "Close dialog", "Tab": "Cycle focus within dialog" }
},
"tokens_used": ["color-bg", "shadow-xl", "radius-xl", "duration-normal"],
"templates": { "html": "<div data-ui=\"dialog\">..." },
"safe_transforms": ["Change title text", "Add body content", "Change trigger text"],
"unsafe_transforms": ["Remove data-ui attribute", "Remove overlay", "Remove focus trap"],
"composition": { "contains": ["button"], "used_in": ["crud-table"] },
"files": { "html": "dialog.html", "css": "dialog.css", "js": "dialog.js", "manifest": "dialog.manifest.json" },
"tests": ["opens on trigger click", "traps focus", "closes on Escape"]
}What Manifests Enable
| Capability | How It Works | |-----------|-------------| | Audit | Validate HTML against slot requirements, variant values, ARIA attributes | | Repair | Auto-fix missing slots, add required ARIA, remove class attributes | | Context | Generate structured JSON/Markdown for AI agents to read | | Explain | Produce human-readable component descriptions with anatomy trees | | Trace | Show dependency graphs, file trees, token usage | | Create | Generate valid component skeletons from the schema |
JavaScript Controllers
Every recipe has a JavaScript controller following the create{Name} factory pattern:
import { createDialog } from "./ui/recipes/dialog/dialog.js";
const el = document.querySelector('[data-ui="dialog"]');
const dialog = createDialog(el);
dialog.open();
dialog.close();
dialog.destroy();Controller Conventions
- Prevent double-init via
root._loom{Name}guard - Find parts via
root.querySelector('[data-part="..."]')selectors - Express state through
data-stateonly — never class names - Return API object with at minimum a
destroy()method - Import only from
core/modules (dom, events, focus, motion, store) - No data fetching — controllers manage UI state, not data
Auto-Initialization
Include loom-core.js and all recipes auto-initialize:
<script src="ui/core/loom-core.js" defer></script>The engine scans for [data-ui] elements matching known recipes, calls their factories, and watches for dynamically added elements via MutationObserver. You never need to call createDialog() manually unless you want the return API.
Data-Driven Rendering
Loom provides two approaches for connecting UI to REST APIs:
l-sourcedirective (built into loom-core.js) — declarative, attribute-basedapiSource()factory (separate script) — imperative, spread intol-data
l-source Directive (Recommended)
Declare a data source directly on any l-data element. Loom injects a reactive array and a CRUD controller into the scope.
<div l-data="{ newTitle: '' }"
l-source:tasks="/api/tasks">
<!-- tasks (array), tasksLoading (bool), tasksError (string|null) are auto-injected -->
<!-- $tasks (controller) provides: load, create, update, remove, refresh, startPolling, stopPolling -->
<template l-if="tasksLoading">
<div data-ui="spinner" data-size="sm"></div>
</template>
<template l-for="task in tasks">
<div data-ui="card" data-size="sm">
<div data-part="body">
<span l-text="task.title"></span>
<button data-ui="button" data-variant="ghost" data-size="sm"
@click="$tasks.remove(task.id)">Delete</button>
</div>
</div>
</template>
<form @submit.prevent="$tasks.create({ title: newTitle }).then(() => newTitle = '')">
<input data-ui="input" l-model="newTitle" placeholder="New task...">
<button data-ui="button" data-variant="primary">Add</button>
</form>
</div>Modifiers
| Modifier | Effect |
|----------|--------|
| .lazy | Don't auto-load on init (call $name.load() manually) |
| .optimistic | Update UI before server confirms (rollback on error) |
| .poll.5000 | Auto-refresh every 5000ms (default 30000ms) |
| .key.uuid | Use uuid as the ID key instead of id |
Example with modifiers: l-source:tasks.optimistic.poll.10000="/api/tasks"
Injected Into Scope
| Name | Type | Description |
|------|------|-------------|
| {name} | Array | The data array |
| {name}Loading | boolean | True during fetch |
| {name}Error | string\|null | Error message |
| ${name} | object | CRUD controller |
Controller methods: load(), create(payload), update(id, payload), remove(id), refresh(), startPolling(ms?), stopPolling()
apiSource() Factory (Legacy)
Loom also ships with apiSource() — a thin data service layer that connects l-data scopes to REST endpoints. It's application-level code (not a Loom controller), so it lives outside the no-fetch audit boundary.
Include
<script src="ui/core/api-source.js"></script>
<script src="ui/core/loom-core.js" defer></script>The apiSource() Factory
apiSource(endpoint, options?)| Option | Default | Description |
|--------|---------|-------------|
| idKey | "id" | Primary key field name |
| pollInterval | 0 | Auto-refresh interval in ms (0 = off) |
| optimistic | true | Update UI before server confirms |
Returns an object meant to be spread into l-data:
| Property | Type | Description |
|----------|------|-------------|
| items | Array | Fetched data |
| loading | boolean | True during initial fetch |
| submitting | boolean | True during a mutation |
| error | string\|null | Error message or null |
| load() | async | GET — fetch all items |
| create(payload) | async | POST — create new item |
| update(id, payload) | async | PATCH — update item by id |
| remove(id) | async | DELETE — remove item by id |
| startPolling(ms?) | — | Start auto-refresh |
| stopPolling() | — | Stop auto-refresh |
| refresh() | async | Alias for load() |
Usage
Spread apiSource() into any l-data scope and call load() on init:
<div l-data="{
...apiSource('/api/tasks', { idKey: 'id', optimistic: true }),
newTitle: ''
}"
l-init="load()">
<!-- Loading state -->
<template l-if="loading">
<div data-ui="spinner" data-size="sm"></div>
</template>
<!-- Error state -->
<template l-if="error">
<span data-ui="text" data-variant="destructive" l-text="error"></span>
<button data-ui="button" data-size="sm" @click="load()">Retry</button>
</template>
<!-- Data-driven list -->
<template l-if="!loading && !error">
<template l-for="task in items">
<div data-ui="card" data-size="sm">
<div data-part="body">
<span l-text="task.title"></span>
<button data-ui="button" data-variant="ghost" data-size="sm"
@click="remove(task.id)">Delete</button>
</div>
</div>
</template>
</template>
<!-- Create -->
<form @submit.prevent="create({ title: newTitle }).then(() => newTitle = '')">
<input data-ui="input" l-model="newTitle" placeholder="New task...">
<button data-ui="button" data-variant="primary">Add</button>
</form>
</div>Optimistic Updates
When optimistic: true (default), the UI updates immediately before the server responds. If the server request fails, the change is rolled back automatically. This makes CRUD operations feel instant.
Polling
<div l-data="{ ...apiSource('/api/notifications', { pollInterval: 15000 }) }"
l-init="load(); startPolling()">
<template l-for="notif in items">
<span l-text="notif.message"></span>
</template>
</div>Multiple Sources
Each apiSource() call is independent — different endpoints, different state:
<script>
const menuSource = apiSource('/api/menus');
const userSource = apiSource('/api/users', { optimistic: false });
</script>
<div l-data="{ ...menuSource }" l-init="load()">
<template l-for="menu in items">
<span l-text="menu.name"></span>
</template>
</div>
<div l-data="{ ...userSource }" l-init="load()">
<template l-for="user in items">
<span l-text="user.email"></span>
</template>
</div>Boundary Rules
apiSource()is application code — lives in a<script>tag or a shared.jsfile- Loom recipe controllers never call
fetch— theno-fetchaudit rule still applies to them - The
l-data/l-init/l-fordirectives bridge data to DOM - Error and loading states use standard Loom components (spinner, card, empty-state)
Dev Server
A Bun-based dev server is included for testing data-driven pages:
bun playground/server.js
# Serves on http://localhost:5555
# API: GET/POST /api/tasks, GET/PATCH/DELETE /api/tasks/:id
# Static: serves playground/ and registry/ filesSee playground/task-manager.html for a full CRUD example using apiSource().
CLI Reference
The CLI is organized into five categories. Run loom help for the full list or loom <command> --help for options.
Project Setup
loom init # Initialize new project (creates ui/, config, bundle)
loom init --theme midnight # Initialize with a specific theme
loom init --tokens-split # Keep token files separate (not merged)
loom init --no-core # Skip JS modules (static CSS-only projects)
loom init --dir ./styles # Custom output directory
loom doctor # Health check (config, files, manifests)Component Management
loom add button card dialog # Add components (auto-resolves dependencies)
loom add --all # Add every component
loom add --layer primitives # Add all primitives
loom add --dry-run # Preview without writing
loom remove dialog toast # Remove components (checks dependencies)
loom remove button --force # Remove even if others depend on it
loom remove card --dry-run # Preview removal
loom list # Show installed and available components
loom create my-widget --kind primitive # Scaffold a new custom component
loom create data-grid --kind recipe # Scaffold with JS controller
loom create status --kind primitive --category layout
loom inspect button # Show manifest details
loom inspect dialog --json # Raw JSON outputDevelopment
loom dev # Start dev server (default: port 3000)
loom dev --port 8080 # Custom port
loom dev --open # Open browser automatically
loom dev --bundle # Auto-rebuild CSS bundle on changes
loom bundle # Generate/regenerate CSS bundle
loom bundle --minify # Strip comments and whitespace
loom bundle --watch # Watch and rebuild on changes
loom bundle --output dist/s.css # Custom output path
loom bundle --dry-run # Show what would be bundled
loom theme set midnight # Switch active theme
loom theme create my-brand # Scaffold custom theme
loom theme list # Show available themes
loom variant add button visual=accent # Add variant value
loom variant remove button visual=accent # Remove variant value
loom scaffold landing-page # Generate landing page HTML
loom scaffold admin-dashboard # Generate dashboard layout
loom scaffold internal-tool # Generate settings/forms pageQuality and Validation
loom audit # Validate all HTML against manifests
loom audit --file index.html # Audit specific file
loom audit --json # JSON output for tooling
loom audit --fix # Alias for repair
loom repair # Auto-fix audit issues
loom conform # Normalize attribute order, add machine comments
loom conform --dry-run # Preview changes
loom trace dialog # Show dependency graph, file tree, token usage
loom trace dialog --json # Machine-readable outputAI / Agent
loom context # Generate .loom/context.json
loom context --format md # Markdown format for LLM prompts
loom context --format cursorrules # Cursor IDE format
loom context --skill # Also generate .loom/SKILL.md
loom context --stdout # Print to stdout
loom explain dialog # Human/agent-readable component explanation
loom explain dialog --json # Structured outputCSS Bundle
The CSS bundle solves the multi-file problem. Without it, a page using all components would need 40-50+ <link> tags. The bundle concatenates everything into one file with correct cascade order.
How It Works
loom bundle reads your loom.config.json, finds all installed components, and concatenates their CSS in this order:
- Tokens — design token custom properties
- Theme — active theme overrides
- Base — reset.css, prose.css
- Primitives — installed primitive CSS (alphabetical)
- Recipes — installed recipe CSS (alphabetical)
- Patterns — installed pattern CSS (alphabetical)
Each section is separated by a /* === primitives/button.css === */ comment for debuggability.
Auto-Bundling
The bundle regenerates automatically when you:
loom add— new components are includedloom remove— removed components are excludedloom theme set— new theme CSS is swapped inloom create— custom component CSS is includedloom init— initial bundle created on project setup
Configuration
After first bundle generation, loom.config.json gains a bundle section:
{
"bundle": {
"output": "./ui/loom.bundle.css",
"auto": true,
"minify": false
}
}Set auto: false to disable auto-regeneration on add/remove/theme changes.
Audit and Repair
The audit system validates your HTML against component manifests. It catches structural errors, missing accessibility attributes, invalid variants, and anti-patterns.
loom audit # Run all checks
loom repair # Auto-fix what can be fixedAudit Rules
| Rule | What It Checks |
|------|---------------|
| slot-satisfied | All required [data-part] slots present |
| valid-attributes | Only manifest-allowed attributes used |
| variant-values | Variant values match manifest's allowed list |
| state-valid | State values match manifest |
| controller-loaded | Recipe controllers are referenced |
| token-exists | CSS tokens used are defined |
| no-fetch | JS controllers don't fetch data |
| no-important | No !important in CSS |
| no-id-selector | No ID selectors in CSS |
| no-class-selector | No class selectors (use data-*) |
| no-external-import | Only relative/core imports in JS |
| reduced-motion | Animations include prefers-reduced-motion query |
Repair
loom repair runs the audit, identifies auto-fixable issues, applies deterministic fixes, then re-audits to verify. Fixable issues include missing ARIA attributes, incorrect attribute order, and missing required slots with obvious defaults.
Conform
loom conform normalizes markup without fixing semantic issues:
- Reorders attributes to canonical order:
data-ui,data-part,data-state,data-variant,data-size, ARIA, then others - Adds machine comments at the top of component CSS files
- Ensures consistent formatting across all HTML files
Scaffolding and Code Generation
Page Scaffolds
Generate complete, working HTML pages with all required CSS and components:
loom scaffold landing-page # Hero + features + CTA sections
loom scaffold admin-dashboard # Sidebar + header + stats + data table
loom scaffold internal-tool # Tab-based settings with formsScaffolds auto-install any missing components and use the bundle when one exists (single <link> tag instead of per-component links).
Custom Components
Create your own components that integrate with the full Loom workflow:
loom create sidebar --kind primitiveThis generates a complete component directory:
ui/primitives/sidebar/
├── sidebar.manifest.json Valid manifest skeleton
├── sidebar.css CSS with [data-ui="sidebar"] selector
└── sidebar.html Reference markupFor recipes (--kind recipe), a JavaScript controller stub is also generated with the create{Name} pattern.
Custom components are immediately registered in loom.config.json, included in the CSS bundle, and visible to loom audit, loom context, and all other CLI tools.
AI Agent Integration
Loom is designed as an agent-native framework. Every design decision optimizes for AI agents being able to reliably generate, inspect, and repair UI code.
How Agents Use Loom
- Read manifests — JSON contracts describe every component's anatomy, slots, variants, states, and ARIA requirements
- Generate markup — Use
templates.htmlfrom manifests as starting points - Audit results — Run
loom auditto validate generated HTML - Auto-repair — Run
loom repairto fix common mistakes - Understand constraints —
safe_transformsandunsafe_transformstell agents what they can and cannot modify
Context Generation
loom context # Generate .loom/context.json
loom context --format md # Markdown for LLM system prompts
loom context --skill # Generate Claude Code SKILL.mdThe context file aggregates all installed component manifests into a single JSON file that agents can read at the start of a session. It includes:
- Framework version and theme
- The five-attribute protocol
- All component kinds, variants, slots, states, templates
- Safe/unsafe transform rules
- Linting constraints
Claude Code Integration
Loom ships with a loom-creator skill for Claude Code. When active, Claude can:
- Generate pages using the correct attribute protocol
- Read manifests to understand component contracts
- Apply the CSS bundle pattern (single
<link>tag) - Follow the strict rules (no classes, tokens only, ARIA compliance)
- Use reactive directives (
l-data,l-model,l-for, etc.)
The skill references are in .claude/skills/loom-creator/references/:
primitives.md— All 22 primitives with full HTML anatomyrecipes.md— All 15 recipes with HTML, JS controller patternspatterns.md— All 6 composition patternstokens.md— Complete design token referencemanifest.md— Manifest JSON schema and examplesdirectives.md— Reactive directives and global API
CSS Conventions
Seven rules govern all component CSS in Loom:
- Semantic CSS, not utility-first. Button styling belongs in
button.css, not scattered across utility classes. - Attribute selectors only.
[data-ui="button"], never.btn. - Token references only.
var(--color-primary), never#4f46e5. - State via
data-state, never classes.[data-state="open"], never.is-open. - No
!important. Low specificity via single attribute selectors makes it unnecessary. - No IDs as CSS selectors. IDs exist for ARIA relationships only (
aria-labelledby,aria-controls). - Respect
prefers-reduced-motion. Every animation has a reduced-motion fallback.
Component CSS Header Convention
/* @ui:component button */
/* @ui:tokens color-primary, color-primary-hover, radius-md, space-4, duration-fast */
[data-ui="button"] {
/* base styles */
}
[data-ui="button"][data-variant="primary"] {
/* variant override */
}
[data-ui="button"][data-state="loading"] {
/* state style */
}Project Structure
loom-ui/
├── src/ CLI source (TypeScript)
│ ├── index.ts Entry point — command router
│ ├── manifest.ts Manifest types and validation
│ ├── commands/ 18 CLI commands
│ │ ├── init.ts Project initialization
│ │ ├── add.ts Component installation
│ │ ├── remove.ts Component uninstallation
│ │ ├── create.ts Custom component scaffolding
│ │ ├── bundle.ts CSS bundle composition
│ │ ├── dev.ts Development server
│ │ ├── list.ts Component listing
│ │ ├── inspect.ts Manifest viewer
│ │ ├── audit.ts HTML validation
│ │ ├── repair.ts Auto-fix engine
│ │ ├── doctor.ts Health checker
│ │ ├── context.ts AI context generator
│ │ ├── explain.ts Component explainer
│ │ ├── trace.ts Dependency tracer
│ │ ├── conform.ts Markup normalizer
│ │ ├── theme.ts Theme manager
│ │ ├── variant.ts Variant editor
│ │ └── scaffold.ts Page generator
│ ├── audit/ Audit subsystem
│ │ ├── rules.ts 12 audit rules
│ │ ├── checker.ts DOM contract checker
│ │ ├── reporter.ts Formatted output
│ │ └── repairer.ts Auto-fix logic
│ ├── parser/ Code parsers
│ │ ├── html-parser.ts Component instance extraction
│ │ ├── css-parser.ts Token and selector extraction
│ │ └── js-parser.ts Import and pattern detection
│ ├── generator/ Code generators
│ │ ├── context.ts .loom/context.json generator
│ │ ├── manifest.ts Manifest aggregator
│ │ └── skill.ts Claude Code skill generator
│ └── utils/ Shared utilities
│ ├── config.ts loom.config.json reader/writer
│ ├── fs.ts File system helpers
│ ├── logger.ts Colored terminal output
│ ├── components.ts Component lookup and registry helpers
│ ├── codegen.ts Shared code generators (loom.js, context.json)
│ └── bundler.ts CSS bundle generator
│
├── registry/ Component library (shipped with CLI)
│ ├── tokens/ 10 CSS token files (incl. document.css, doc-aliases.css)
│ ├── base/ reset.css, prose.css
│ ├── core/ loom-core.js, api-source.js + utility modules
│ ├── themes/ 5 built-in themes (incl. document.css)
│ ├── primitives/ 30 CSS-only components
│ ├── recipes/ 16 CSS+JS interactive components
│ └── patterns/ 7 page-level compositions
│
├── tests/ Bun test suite (462 tests)
├── playground/ 6 example pages + dev server (server.js, db.json)
├── package.json
├── tsconfig.json
└── loom.config.json (generated per-project)Development
Prerequisites
- Bun (runtime, package manager, test runner)
Setup
git clone <repo-url>
cd loom-ui
bun installCommands
bun test # Run all 462 tests
bun run src/index.ts help # CLI help
bun run src/index.ts dev # Start dev server for playground
tsc --noEmit # Type checkAdding a New Primitive
- Create
registry/primitives/{name}/with.html,.css,.manifest.json - Follow the manifest schema in
src/manifest.ts - Use attribute selectors and token references in CSS
- Add tests in
tests/
Adding a New Recipe
Same as primitive, plus:
- Add a
.jscontroller withexport function create{Name}(root) { ... } - Follow the controller conventions (double-init guard, data-state only, destroy API)
- The controller is auto-registered in
loom-core.js
Tech Stack
| Layer | Technology | |-------|-----------| | Runtime | Bun (TypeScript-first, ESM) | | Testing | Bun test + Happy-DOM | | Styling | Pure CSS with custom properties (oklch colors) | | Reactivity | Custom proxy-based engine (loom-core.js, ~3000 lines) | | Dependencies | Zero at runtime. TypeScript + Happy-DOM for development |
License
MIT
