# Domphy — Full LLM Context
> One-shot dump for code generation. Contains: critical rules, quickstart, core runtime docs, theme docs, and every `@domphy/ui` patch source. Prefer the curated `llms.txt` index for targeted lookups.
---
## Critical rules
- Build UIs as plain objects keyed by HTML tag. Apply patches via `$`. Never wrap in components.
- Never inline typography styles. Use typography patches: `small()`, `paragraph()`, `heading()`, `link()`, `strong()`, `emphasis()`, `code()`, `keyboard()`.
- For forms, compose `form()` + `field()`. Do NOT wire `FormState`/`FieldState` manually.
- `field` patch `value`/`checked` must be static defaults, never reactive — reactive bindings loop forever.
- Build tool: tsup. Docs: VitePress.
---
## Landing
# Domphy
**A Patch-based UI. Native elements, no components.**
Domphy is split into 3 packages:
- `@domphy/core` - `30kb` minified
- `@domphy/theme` - `8kb` minified
- `@domphy/ui` - `80kb` minified
In practical terms:
- `@domphy/core` is the runtime layer, roughly comparable to `react-dom` + SSR rendering + CSS-in-JS in one package
- `@domphy/theme` and `@domphy/ui` together are the design-system layer, roughly comparable to what people usually expect from MUI
Domphy removes component boundaries, unifies SSR and CSR under one model, automates context-aware styling, and works with any JavaScript library without adapters or plugins.
## Why Domphy
From the author:
> I published Domphy in February 2026, at 41 years old. I spent 10 years as a structural architect and 6 years teaching myself to code (2 years with js/ts). Every time I tried to learn React or Vue, something felt wrong: logic scattered between data and UI, too many abstractions, too many plugins just to ship a feature. So I built what I wished existed.
> I introduce Patch-based UI Architecture, a paradigm for composing web interfaces distinct from component-based, directive-based, and mixin-based approaches. A Patch is formally defined as a function returning a PartialElement: a composable, stateless descriptor that augments a host element's behavior without wrapping, replacing, or owning it. Unlike existing composition models, a Patch carries no rendering lifecycle, holds no state, and creates no DOM boundary.
## Installation
npm install @domphy/ui
```
```html [CDN]
```
## Quick Start
// source: /docs/demos/core/counting.ts
---
## Quickstart
# 5-Minute Quickstart
## Install
npm install @domphy/ui
```
```html [CDN]
```
`@domphy/ui` includes `@domphy/core` and `@domphy/theme` — one install gives you everything.
## 1. Hello World
A Domphy element is a plain object. The key is the HTML tag, the value is the content.
// source: /docs/demos/quickstart/01-hello.ts
No classes, no components, no JSX. Just objects.
## 2. Add Patches
A **patch** is a function that adds styling and behavior to an element. Apply it with the `$` property.
// source: /docs/demos/quickstart/02-patches.ts
Every patch handles its own sizing, spacing, colors, and accessibility. You write the structure — patches do the rest.
## 3. Reactive State
Use `toState()` for reactive values. Read with `state.get(listener)` inside a reactive function to auto-subscribe.
// source: /docs/demos/quickstart/03-state.ts
No virtual DOM, no diffing. Changing state updates only the properties that read it.
## 4. Forms
The `form` and `field` patches handle validation, error display, and two-way binding automatically.
// source: /docs/demos/quickstart/04-form.ts
## What's Next
- [Core concepts](/docs/core/) — Syntax, reactivity, lifecycle
- [Theme](/docs/theme/) — Tone, size, density
- [All 69 patches](/docs/ui/) — Buttons, inputs, cards, dialogs, and more
- [Showcase: Chromametry App](https://chromametry.com) — Full production app built entirely with Domphy
- [Research](/docs/research/) — The two papers behind the design system
---
## Core docs
### index.md
# Core
`@domphy/core` is the runtime of Domphy.
It takes plain JavaScript objects and turns them into:
- DOM on the client
- HTML and CSS for SSR
- reactive updates after state changes
- lifecycle and patch execution
If you only want to understand "how Domphy works", this is the package to learn first.
## What You Write
In Domphy, an element is just a plain object.
```ts
{
button: "Count: 0",
class: "primary",
onClick: () => console.log("clicked"),
}
```
You do not write templates or JSX here. You describe the UI with objects, and `@domphy/core` handles rendering, updates, and SSR.
## Quick Start
Client rendering starts by wrapping your root element with `ElementNode`, then calling `render()` with a DOM target.
```ts
import { ElementNode } from "@domphy/core"
const App = {
h1: "Hello Domphy",
}
const root = new ElementNode(App)
root.render(document.getElementById("app")!)
```
That is the core entry point:
- define a plain object tree
- create an `ElementNode`
- render it into the DOM
## What Core Does
When you create an `ElementNode`, core handles the full runtime for that tree:
- reads the object shape
- creates `ElementNode` and `TextNode` instances
- renders DOM elements and text nodes
- generates scoped CSS from `style`
- tracks reactive functions
- runs hooks such as `_onInit`, `_onMount`, and `_onBeforeRemove`
- can generate HTML and CSS for SSR
## Package Boundary
The packages have different jobs:
| Package | Role |
| --- | --- |
| `@domphy/core` | object syntax, rendering, reactivity, lifecycle, SSR |
| `@domphy/theme` | design tokens and theme CSS |
| `@domphy/ui` | ready-made patches such as `button()`, `dialog()`, `tabs()` |
If you are learning Domphy from the bottom up, start with `core`, then move to `theme` and `ui`.
## Read In This Order
Use this order if you are learning core for the first time:
1. [Syntax](./syntax) - object shape, reserved keys, attributes, style, events, hooks
2. [Reactivity](./reactivity) - how listener-based updates work
3. [Lifecycle](./lifecycle) - when hooks run and what each hook is for
4. [SSR](./ssr) - `generateHTML()`, `generateCSS()`, `render()`, and `mount()`
5. [Portal](./portal) - render DOM outside the logical parent
6. [Insert Content](./patterns/insert-content) - practical patterns for adding and updating children
If you need exact method signatures, use the API Reference in the sidebar after reading these guides.
### lifecycle.md
# Lifecycle
Hooks fire in a fixed linear sequence. Each step exposes more of the node as it progresses.
```ts
import { DomphyElement, merge } from "@domphy/core"
const App: DomphyElement<"div"> = {
div: "Hello",
_onSchedule: (node, rawElement) => {
const theme = node.getContext("theme")
merge(rawElement, {
style: {
color: theme === "dark" ? "#fff" : "#000",
},
})
},
_onInsert: (node) => {
const index = node.parent!.children.items.indexOf(node)
node.setMetadata("index", index)
},
_onMount: (node) => {
const observer = new ResizeObserver(() => {
console.log(node.domElement!.offsetWidth)
})
observer.observe(node.domElement!)
node.addHook("BeforeRemove", () => observer.disconnect())
},
_onBeforeUpdate: (node, rawChildren) => {
console.log("incoming:", rawChildren.length)
},
_onUpdate: (node) => {
console.log("children updated:", node.children.items.length)
},
_onBeforeRemove: (node, done) => {
node.domElement!
.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 300 })
.onfinish = done
},
_onRemove: () => {
console.log("removed")
},
}
```
## Hook Order
| Hook | When | What's available |
| --- | --- | --- |
| `_onSchedule(node, raw)` | Before parsing, while the raw element can still be changed | `parent`, `_context`, `_metadata`, mutable `raw` |
| `_onInit(node)` | After parsing, before insertion | node properties, no siblings yet |
| `_onInsert(node)` | Added to the parent child list | siblings, position in tree |
| `_onMount(node)` | DOM element created and connected to the node | `domElement` and all node properties |
| `_onBeforeUpdate(node, rawChildren)` | Before a child update cycle applies new children | current node, current DOM, incoming raw children |
| `_onUpdate(node)` | After the update cycle finishes | updated children and `domElement` |
| `_onBeforeRemove(node, done)` | Before removal, must call `done()` | `domElement`, current runtime state |
| `_onRemove(node)` | After the node is fully removed | node instance after removal work completes |
`_onSchedule` is the right place to apply context-aware patches. Unlike inline `$: [patches]`, it can read parent context before parsing begins.
See also [ElementNode API](./api/element-node).
### portal.md
# Portal
`_portal` redirects where an element's DOM renders — the element stays in the logical Domphy tree, but its DOM node is appended to a different parent.
```ts
{
div: "Tooltip content",
_portal: () => document.body,
}
```
`rootNode` is the app root `ElementNode`. Use it to query or insert overlay containers:
```ts
{
div: "...",
_portal: (rootNode) => {
let overlay = rootNode.domElement!.querySelector("#my-overlay")
if (!overlay) {
overlay = document.createElement("div")
overlay.id = "my-overlay"
rootNode.domElement!.appendChild(overlay)
}
return overlay
},
}
```
## Why Use Portals
Without `_portal`, fixed or absolute overlays inherit `overflow: hidden` and stacking context from ancestor elements, which causes clipping and layering bugs.
## Properties
- The element's logic remains tied to its position in the Domphy tree
- Only the DOM node moves
- CSS generated by the element is still injected normally
- `_portal` is evaluated once at mount time
Use it for overlays that must escape ancestor layout constraints: tooltips, dropdowns, toasts, and modal layers.
### reactivity.md
# Reactivity
Domphy uses listener-based reactivity. Any value can be a function that receives a `listener`. When a subscribed state changes, Domphy re-runs only that reactive part.
```ts
const count = toState(0)
const counter = {
button: (listener) => `Count: ${count.get(listener)}`,
onClick: () => count.set(count.get() + 1),
}
```
`count.get(listener)` does two things:
- returns the current value
- subscribes that reactive function to future changes
Subscriptions are released automatically when the node is removed.
// source: /docs/demos/core/counting.ts
## Attributes
Reactive attributes are already fine-grained. When the state changes, Domphy updates only that attribute.
```ts
const open = toState(false)
const button = {
button: "Toggle",
ariaExpanded: (listener) => open.get(listener),
disabled: (listener) => !open.get(listener),
}
```
This does not re-create the node. It only updates the affected DOM attributes.
Use reactive attributes for:
- `disabled`
- `hidden`
- `value`
- `aria-*`
- `data-*`
- any attribute whose value should track state directly
## CSS Props
Reactive CSS properties are also fine-grained. Domphy updates only the specific CSS declaration that changed.
```ts
const active = toState(false)
const box = {
div: "Hello",
style: {
color: (listener) => active.get(listener) ? "red" : "gray",
opacity: (listener) => active.get(listener) ? 1 : 0.5,
},
}
```
This is different from re-rendering the whole node. The existing style rule stays mounted; only the changed CSS properties are updated.
Use reactive style props when:
- the element itself stays the same
- only visual state changes
- you want the smallest possible DOM/CSS update
## Children Update
Reactive children are more complex than attributes or CSS props. When the child function runs again, Domphy calls `children.update(...)` and reconciles the child list.
```ts
const items = toState([
{ id: 1, name: "A" },
{ id: 2, name: "B" },
])
const list = {
ul: (listener) => items.get(listener).map(item => ({
li: item.name,
_key: item.id,
})),
}
```
### Default Rerender
For light children such as text or simple unkeyed content, the default reactive child update is usually enough.
```ts
const count = toState(0)
const app = {
p: (listener) => `Count: ${count.get(listener)}`,
}
```
This is the simplest form and should be the default choice for simple text children or lightweight child trees.
### Fine-Grain With `_key`
When children are dynamic lists, `_key` gives Domphy a reconciliation identity.
```ts
const list = {
ul: (listener) => items.get(listener).map(item => ({
li: item.name,
_key: item.id,
})),
}
```
`_key` is used only for child diffing. If the key matches, Domphy reuses the existing node instance and DOM node instead of creating a new one.
Use `_key` when:
- items can reorder
- items can insert in the middle
- items can be removed from the middle
- child instances carry important runtime behavior
Without `_key`, child diffing is more positional.
### Fine-Grain With Low-Level API
For the most control, update the child list imperatively through the `ElementList` API instead of relying on a reactive child function to rebuild the array.
```ts
const app = {
div: [
{
button: "Add child",
_onInit: (node) => {
node.addEvent("click", () => {
node.parent!.children.insert({ span: "New child" })
})
},
},
],
}
```
Or inside a normal event handler:
```ts
{
button: "Add child",
onClick: (_, node) => {
node.parent!.children.insert({ span: "New child" })
},
}
```
This is also fine-grained:
- `insert()` creates only the new child
- `remove()` removes only that child
- `move()` reorders existing children
- `swap()` swaps existing children
Use the low-level API when updates are event-driven and local, and when you want explicit control over exactly which child changes.
## External State Systems
Domphy does not enforce a state architecture. Any system that can call a function works:
```ts
store.subscribe(() => listener()) // Zustand
atom.subscribe(() => listener()) // Nanostores
count$.subscribe(() => listener()) // RxJS
```
→ [State API](./api/state)
## Not To Do
- Do not create reactive update loops where one reactive read immediately feeds an event that writes the same source again without a clear boundary.
```ts
const text = toState("")
const field = {
input: null,
value: (listener) => text.get(listener),
onChange: (event) => text.set((event.target as HTMLInputElement).value),
}
```
- Do not think of this as two-way binding; treat it as one-way data flow instead, where state drives the view and events explicitly write the next state.
```ts
const text = toState("")
const field = {
input: null,
value: (listener) => text.get(listener),
onInput: (event) => {
text.set((event.target as HTMLInputElement).value)
},
}
```
- Do not move ordinary form synchronization into hooks; keep it in flat event handlers such as `onInput`, `onChange`, or `onClick` so the read path and write path stay visible.
### ssr.md
# SSR
Domphy uses the same element definition for CSR and SSR — no duplicate templates.
## Client Render
```ts
new ElementNode(App).render(document.body)
```
Call `render()` once at the app root for client-side rendering.
## Server-Side Rendering
import { ElementNode } from "@domphy/core"
import { themeCSS } from "@domphy/theme"
import App from "./app.js"
const node = new ElementNode(App)
const page = `