5-Minute Quickstart
Install
npm install @domphy/ui
@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.
import type { DomphyElement } from "@domphy/core";
import { themeApply } from "@domphy/theme";
themeApply();
const App: DomphyElement<"div"> = {
div: [
{ h1: "Hello, Domphy" },
{ p: "A plain object becomes a real DOM element." },
],
};
export default App;
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.
import type { DomphyElement } from "@domphy/core";
import { button, card, heading, paragraph, tag } from "@domphy/ui";
const App: DomphyElement<"div"> = {
div: [
{ h2: "Patches in action", $: [heading()] },
{
div: [
{ h4: "What is a patch?", $: [heading()] },
{
p: "A function that adds styling, spacing, and behavior to any element.",
$: [paragraph()],
},
{ aside: "new", $: [tag({ color: "success" })] },
{
footer: [
{ button: "Primary", $: [button({ color: "primary" })] },
{ button: "Default", $: [button()] },
],
},
],
$: [card()],
},
],
};
export default App;
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.
import { type DomphyElement, toState } from "@domphy/core";
import { button, heading } from "@domphy/ui";
const count = toState(0);
const App: DomphyElement<"div"> = {
div: [
{ h3: (l) => `Count: ${count.get(l)}`, $: [heading()] },
{
button: "Increment",
onClick: () => count.set(count.get() + 1),
$: [button({ color: "primary" })],
},
{
button: "Reset",
onClick: () => count.set(0),
$: [button()],
},
],
style: { display: "flex", gap: "8px", alignItems: "center" },
};
export default App;
No virtual DOM, no diffing. Changing state updates only the properties that read it.
4. Forms
Form state, validation, and submission live in @domphy/form (createForm) — a 1-1 port of @tanstack/form-core. @domphy/ui provides the presentation: native inputs with the input patches, label, and formGroup for layout. Bind each field with value: (l) => field.value(l) and forward events to field.handleChange(...).
import type { DomphyElement } from "@domphy/core";
import { createForm } from "@domphy/form/domphy";
import { button, formGroup, inputText, label } from "@domphy/ui";
const myForm = createForm<{ name: string; email: string }>({
defaultValues: { name: "", email: "" },
onSubmit: ({ value }) => alert(JSON.stringify(value, null, 2)),
});
const name = myForm.field<string>("name");
const email = myForm.field<string>("email");
const App: DomphyElement<"form"> = {
form: [
{
div: [
{ label: "Name", $: [label()] },
{
input: null,
type: "text",
placeholder: "Enter your name",
$: [inputText()],
value: (l) => name.value(l),
onInput: (e) =>
name.handleChange((e.target as HTMLInputElement).value),
},
],
$: [formGroup()],
},
{
div: [
{ label: "Email", $: [label()] },
{
input: null,
type: "email",
placeholder: "you@example.com",
$: [inputText()],
value: (l) => email.value(l),
onInput: (e) =>
email.handleChange((e.target as HTMLInputElement).value),
},
],
$: [formGroup()],
},
{
button: "Submit",
type: "submit",
$: [button({ color: "primary" })],
},
],
onSubmit: (e: Event) => {
e.preventDefault();
myForm.handleSubmit();
},
_onRemove: () => myForm.destroy(),
};
export default App;
What's Next
- Core concepts — Syntax, reactivity, lifecycle
- Theme — Tone, size, density
- All 74 patches — Buttons, inputs, cards, dialogs, and more
- Research — The two papers behind the design system