5-Minute Quickstart

Install

<div class="blocks">
<div class="block active" data-tab="0">
npm install @domphy/ui
</div>
<div class="block" data-tab="1">

</div>
</div>

@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