---
name: dom-creation
description: How to create DOM elements in rikka — `h()` function, 200+ tag helpers (`div`, `p`, `button`, …), and the `h\`html\`` template literal. Load this when the task is about constructing elements, the `h` API, mounting elements into the document, or HTML/SVG namespace handling.
---

# DOM Creation

`h()` and tag helpers create **real `Element` objects** — no virtual DOM, no factory, no second call. Append the result directly to the DOM.

## Imports

```typescript
import { h, div, p, button, span, For, Show, When, Switch, Match, css, inlineStyle } from "@takanashi/rikka-dom";
```

## `h(tag, attrs?, ...children): Element`

```typescript
import { h } from "@takanashi/rikka-dom";

const el = h("div", { class: "container" }, h("p", "Hello"));
// el instanceof HTMLDivElement === true

document.body.appendChild(el);
```

The first argument can be:

- A string tag name (`"div"`, `"my-element"`, …)
- A custom element constructor (see [../composition/](../composition/))

The optional second argument is an attrs object. Remaining arguments are children.

## Tag helpers

Over 200 pre-defined tag functions share the same signature as `h()` minus the tag parameter. They return real `Element`:

```typescript
import { div, p, button, span, input } from "@takanashi/rikka-dom";

const card = div(
  { class: "card" },
  h2({}, "Title"),
  p({}, "Body"),
  button({ onclick: () => alert("clicked") }, "OK"),
);

document.body.appendChild(card);
```

The full set:

| Group | Tags |
|-------|------|
| HTML | `a`, `abbr`, `address`, `area`, `article`, `aside`, `audio`, `b`, `base`, `bdi`, `bdo`, `blockquote`, `body`, `br`, `button`, `canvas`, `caption`, `cite`, `code`, `col`, `colgroup`, `data`, `datalist`, `dd`, `del`, `details`, `dfn`, `dialog`, `div`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `figure`, `font`, `footer`, `form`, `h1`–`h6`, `head`, `header`, `hgroup`, `hr`, `html`, `i`, `iframe`, `img`, `input`, `ins`, `kbd`, `label`, `legend`, `li`, `link`, `main`, `map`, `mark`, `menu`, `meta`, `meter`, `nav`, `noscript`, `object`, `ol`, `optgroup`, `option`, `output`, `p`, `picture`, `pre`, `progress`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `script`, `search`, `section`, `select`, `slot`, `small`, `source`, `span`, `strong`, `style`, `sub`, `summary`, `sup`, `table`, `tbody`, `td`, `template`, `textarea`, `tfoot`, `th`, `thead`, `time`, `title`, `tr`, `track`, `u`, `ul`, `var_`, `video`, `wbr` |
| SVG (no prefix — auto-resolves namespace) | `svg`, `animate`, `animateMotion`, `animateTransform`, `circle`, `clipPath`, `defs`, `desc`, `ellipse`, `feBlend`, `feColorMatrix`, `feComponentTransfer`, `feComposite`, `feConvolveMatrix`, `feDiffuseLighting`, `feDisplacementMap`, `feDistantLight`, `feDropShadow`, `feFlood`, `feFuncA`, `feFuncB`, `feFuncG`, `feFuncR`, `feGaussianBlur`, `feImage`, `feMerge`, `feMergeNode`, `feMorphology`, `feOffset`, `fePointLight`, `feSpecularLighting`, `feSpotLight`, `feTile`, `feTurbulence`, `filter`, `foreignObject`, `g`, `image`, `line`, `linearGradient`, `marker`, `mask`, `metadata`, `mpath`, `path`, `pattern`, `polygon`, `polyline`, `radialGradient`, `rect`, `set`, `stop`, `switch_`, `symbol`, `text`, `textPath`, `tspan`, `use`, `view` |
| MathML (no prefix — auto-resolves namespace) | `math`, `annotation`, `annotationXml`, `maction`, `menclose`, `merror`, `mfenced`, `mfrac`, `mglyph`, `mi`, `mlabeledtr`, `maligngroup`, `malignmark`, `mmultiscripts`, `mn`, `mo`, `mpadded`, `mphantom`, `mprescripts`, `mroot`, `mrow`, `ms`, `mspace`, `msqrt`, `mstyle`, `msub`, `msubsup`, `msup`, `mtable`, `mtd`, `mtext`, `mtr`, `munder`, `munderover`, `none`, `semantics` |
| SVG-prefixed (HTML/SVG name collisions) | `svga`, `svgscript`, `svgstyle`, `svgtitle`, `svgtext`, `svgspan`, `svgtextPath` |

> `var_`, `switch_` use a trailing underscore because `var` and `switch` are JavaScript reserved words. Use `import { var_ as v } from "@takanashi/rikka-dom"` if you need the un-suffixed name.

See [../svg/](../svg/) for namespace handling.

## `h\`html\`: Element[]` (template literal)

HTML template literal with signal interpolation. **Returns an `Element[]`, not a single element.** Use `[0]` to get a single element.

```typescript
import { h } from "@takanashi/rikka-dom";
import { signal } from "@takanashi/rikka-signal";

const name = signal("World");
const elements = h`<span>Hello ${name}!</span>`;
// elements[0] instanceof HTMLSpanElement === true

document.body.appendChild(elements[0]);
```

### Signal interpolation modes

| Syntax | Mode | Behavior |
|--------|------|----------|
| `${signal}` | Fine-grained | Creates an effect; only the text/attr node updates. Preserves focus, cursor, scroll position. |
| `${signal.get()}` | Lost reactivity | Resolved immediately to a static value. |
| `computed(() => h\`...\`)` | Coarse-grained | Entire template rebuilds when a dependency changes. |

```typescript
const count = signal(0);

// ✅ Fine-grained: only the digit updates
h`<span>Count: ${count}</span>`;

// ❌ Static: "Count: 0" forever
h`<span>Count: ${count.get()}</span>`;

// Coarse-grained: entire <span> rebuilds (use for structural changes)
const tmpl = computed(() => h`<span>Count: ${count.get()}</span>`);
```

Attribute interpolation works the same way:

```typescript
const color = signal("red");
h`<div style="color: ${color}">Text</div>`;
// color change → style attribute updates
```

### Getting an `HTMLTemplateElement`

Use `h\`<template>...</template>\`[0]` to get a real `HTMLTemplateElement` for `defineElement`'s `template` option:

```typescript
const tmpl = h`<template><div>{{name}}</div></template>`[0];
tmpl instanceof HTMLTemplateElement; // true
tmpl.content;                         // DocumentFragment containing <div>
```

## Mounting the app

`h()` returns a real `Element`, so use standard DOM APIs:

```typescript
import { div, p, button } from "@takanashi/rikka-dom";
import { signal } from "@takanashi/rikka-signal";

const count = signal(0);

const app = div(
  {},
  p({}, "Count: ", count),
  button({ onclick: () => count.set(count.get() + 1) }, "+"),
);

document.body.appendChild(app);
```

`rikka-dom` also exports `applyChild` for cases where you need to apply a child without `appendChild` (e.g. tests):

```typescript
import { applyChild } from "@takanashi/rikka-dom";

const container = document.getElementById("app");
if (container) applyChild(container, app);
```

## Children

The `Child` type accepts: `null | string | number | boolean | Element | DocumentFragment | ReactiveRange | Signal.State<Child> | Signal.Computed<Child> | Child[] | Signal.State<Child[]> | Signal.Computed<Child[]> | (() => Child)`.

Booleans (`true`/`false`) render nothing — useful for `&&` conditional patterns like `div({}, show && p({}, "visible"))`.

The interesting case is the function — it's auto-wrapped as `computed` so any `.get()` inside is tracked. See [../signal-binding/#function-children](../signal-binding/SKILL.md#function-children) for details.

```typescript
const count = signal(0);

// Function child — equivalent to computed(() => `count=${count.get()}`)
div({}, () => `count=${count.get()}`);

// Function returning null clears the content
const visible = signal(true);
div({}, () => (visible.get() ? span({}, "on") : null));
```

## Namespace handling

rikka-dom auto-selects the namespace based on tag name:

- HTML tags → `http://www.w3.org/1999/xhtml`
- SVG-only tags (`circle`, `path`, `g`, …) → `http://www.w3.org/2000/svg`
- MathML-only tags → `http://www.w3.org/1998/Math/MathML`
- Name-collision tags (`a`, `script`, `style`, `title`, `text`, `span`, `textPath`) → use the `svg` prefix in SVG context: `svga`, `svgscript`, `svgstyle`, `svgtitle`, `svgtext`, `svgspan`, `svgtextPath`

See [../svg/](../svg/).

## Pitfalls

### 1. `h\`\`` returns an array

```typescript
// ❌ TypeError: argument is not a Node
document.body.appendChild(h`<div>Hello</div>`);

// ✅ Take [0]
document.body.appendChild(h`<div>Hello</div>`[0]);
```

### 2. `NumberAttr` default is `NaN` (not `0`)

`Number(undefined)` returns `NaN`. If you want a default, declare it:

```typescript
defineElement("my-el", {
  attributes: { count: { ...NumberAttr, default: 0 } },
});
```

Or use a custom `toProp`:

```typescript
attributes: {
  count: { toProp: (v) => (v !== undefined ? Number(v) : 0) },
}
```

### 3. Never use `innerHTML`

It bypasses the signal system, has XSS risk, and prevents binding:

```typescript
// ❌ No reactivity, XSS-prone
el.innerHTML = `<div>${title}</div>`;

// ✅
el.replaceChildren(div({}, title));
```

### 4. Plain function as a value (not a child) is NOT reactive

Function *children* are auto-wrapped as `computed`, but function *values* (e.g. as an attribute, or in `inlineStyle` arg) are not:

```typescript
// ✅ Function child — auto-wrapped
div({}, () => `count=${count.get()}`);

// ❌ Plain function in inlineStyle — NOT reactive
div({ style: { color: () => count.get() % 2 ? "red" : "blue" } });

// ✅ Use computed() for value-position reactivity
div({ style: { color: computed(() => count.get() % 2 ? "red" : "blue") } });
```

See [../signal-binding/#function-values-vs-computed](../signal-binding/SKILL.md#function-values-vs-computed).

## See also

- [../signal-binding/](../signal-binding/) — putting signals into the DOM
- [../control-flow/](../control-flow/) — `For`, `Show`, `When`, `Switch`
- [../form-binding/](../form-binding/) — two-way binding inputs
- [../svg/](../svg/) — SVG markup
- [../common-pitfalls/](../common-pitfalls/)
