flub.jsv2 α
FIELDGUIDE№ 002

A tiny reactive DOM framework

Reactive UI.
Native DOM.

Components create native nodes once. Signals update only their dependents. No virtual DOM, compiler, or browser-runtime dependencies.

Get started ↓
00runtime dependencies
03browser engines tested
01real DOM tree
01

First contact

Components return DOM nodes.

Call a component once. Use an accessor for values that should update.

npm install flub.js@next

The counter creates its DOM once. Each click updates one text node.

Live signal

0
counter.ts
import { Btn, Text, mount, signal } from "flub.js";

mount(document.body, Counter);

function Counter() {
  const count = signal(0);

  return [
    Text({ children: () => `Count: ${count()}` }),
    Btn({
      children: "+",
      onClick: () => count.update(n => n + 1),
    }),
  ];
}
02

Fine-grained state

Signals track dependencies.

Reads subscribe. Writes notify. Effects batch into a microtask; computed values stay lazy.

signal()

Callable state with set, update, and peek.

computed()

Lazy, cached derived state.

effect()

Owned work with cleanup before reruns.

batch()

Multiple writes, one flush.

reactivity.ts
const first = signal("Ada");
const last = signal("Lovelace");
const full = computed(() => `${first()} ${last()}`);

effect(() => console.log(full()));

batch(() => {
  first.set("Grace");
  last.set("Hopper");
}); // the effect runs once: “Grace Hopper”
03

Direct rendering

Updates go directly to the DOM.

Elements are native immediately. Reactive bindings preserve node identity, focus, selection, and browser state.

Update path / 01

sourcecount.set(2)
↓ notify
accessor() => `Count: ${count()}`
↓ mutate
native nodeText.data = "Count: 2"

One dependency, one DOM write.

A signal updates its dependent text, property, class, style, or structural region.

  • Native boolean and form-control properties
  • Namespace-correct SVG
  • Event listeners with deterministic cleanup
  • Raw innerHTML intentionally rejected
bindings.ts
Input({
  value: name,
  disabled: () => name() === "",
  class: { invalid: () => !isValid() },
  style: { opacity: () => saving() ? 0.5 : 1 },
  onInput: event => name.set(event.currentTarget.value),
});

Optional build step

Start with static HTML.

@flub/static can render selected pages during a Vite build. Normal mount() applications do not change.

Add enhance() for behavior on existing nodes, or mount independent interactive islands. No hydration pass is required.

vite.config.ts + page.ts
// vite.config.ts
import { flubStatic } from "@flub/static/vite";

export default {
  plugins: [flubStatic({
    pages: { "index.html": "src/page.ts" },
  })],
};

// src/page.ts
export default function Page() {
  return Main({ children: "Rendered at build time" });
}
04

Elements + components

A native toolbox, not a theme.

Element factories expose the platform with exact types. Components add small layout, accessibility, and ownership policies without prescribing appearance.

Choose an entry point

Import only the layer you need.

01

flub.js/elements

Typed native factories

02

flub.js/components

Unstyled composition

03

flub.js

Everything from one entry

HTML factory index

Every modern element.

112 / 112

Document + metadata

12
HtmlHeadBodyBaseLinkElementMetaStyleTitleNoscriptScriptTemplateSlot

Sections + headings

17
AddressArticleAsideDivFooterH1H2H3H4H5H6HeaderHgroupMainNavSearchSection

Text semantics

38
AAbbrBBdiBdoBlockquoteBrCiteCodeDataDdDelDfnDlDtEmHrIInsKbdMarkPPreQRpRtRubySSampSmallSpanStrongSubSupTimeUVarWbr

Forms + feedback

17
ButtonDatalistDetailsDialogFieldsetFormInputLabelLegendMeterOptgroupOptionOutputProgressSelectSummaryTextarea

Media + embeds

14
AreaAudioCanvasEmbedFigcaptionFigureIframeImgMapElementObjectElementPictureSourceTrackVideo

Lists + tables

14
CaptionColColgroupLiMenuOlTableTbodyTdTfootThTheadTrUl

All factories accept native ElementProps. H() selects a heading level at runtime; Svg() covers the SVG namespace.

elements.ts
import { Article, H2, P } from "flub.js/elements";

const Feature = Article({
  class: "feature",
  children: [
    H2({ children: "Native by default" }),
    P({ children: "Every helper returns its exact DOM type." }),
  ],
});
layout.ts
import { Cluster, Grid, Heading } from "flub.js/components";

Grid({
  columns: "repeat(auto-fit, minmax(14rem, 1fr))",
  gap: 16,
  children: cards.map(card =>
    Cluster({ gap: 8, children: [Heading(card), card.body] }),
  ),
});

Live component workbench

Compose first. Style outside.

UNSTYLED / 01

Field wiring

labels + hints + values

Shown to collaborators.
Nothing submitted yet.

Native patterns

layout + disclosure + dialog

01

Grid

02

Gap

03

Flow
Why no component CSS?

The library owns behavior and semantics. This guide supplies the visual system you see here.

ModalDialog(open, onDismiss)

Native dialog

Controlled by a signal.

The browser provides modality and focus behavior. flub synchronizes open state and lifecycle cleanup.

form.ts
ManagedForm({
  fields: [
    TextInput({ name: "name", label: "Name", hint: "Publicly visible" }),
    SelectInput({ name: "role", label: "Role", items: roles }),
    Switch({ name: "updates", label: "Product updates" }),
  ],
  onSubmit: (_event, values) => save(values),
});
dialog.ts
const open = signal(false);

ModalDialog({
  open,
  onDismiss: () => open.set(false),
  "aria-labelledby": "dialog-title",
  children: [
    Heading({ id: "dialog-title", children: "Native dialog" }),
    Btn({ onClick: () => open.set(false), children: "Close" }),
  ],
});

Component-only props

Small policies, native escape hatches.

These are the props flub adds. Every component also forwards the native props of its returned element.

01

Layout

Box · Flex · Row · Column · Grid · Cluster · Container · Center · Spacer · AspectRatio

Compose common CSS layouts while retaining native div props.

adds: gap · direction · wrap · columns · rows · maxWidth · gutter · ratio

02

Typography

Text · Heading · CodeBlock · Separator · VisuallyHidden

Choose semantic text and accessibility behavior without theme styles.

adds: as · level · code · language · orientation

03

Actions

Btn · Link · IconButton

Safer defaults and accessible names on native interactive elements.

adds: type defaults to button · href required · label required

04

Fields

Field · TextInput · EmailInput · PasswordInput · SearchInput · NumberInput · TextareaInput · SelectInput

Connect labels, descriptions, validation state, and native controls.

adds: label · hint · error · fieldProps · items

05

Choices + forms

Checkbox · Radio · Switch · ManagedForm

Label choice controls and collect repeated form values safely.

adds: label · hint · fields · onSubmit(values, data)

06

Collections

List · OrderedList · DescriptionList · Breadcrumbs

Render keyed records inside the correct semantic container.

adds: items · render · key · fallback · term · details · separator

07

Disclosure + status

Disclosure · ModalDialog · Alert · Status

Use native disclosure, dialog, and live-region semantics.

adds: summary · open · onDismiss · modal

05

Dynamic structure

Conditional and keyed regions.

Show owns one branch. For moves keyed node ranges and disposes removed records.

Keyed identity lab

4 records
  1. 01Signalskey:1
  2. 02Ownerskey:2
  3. 03DOM rangeskey:3
  4. 04Cleanupkey:4
keyed-list.ts
For(
  todos,
  (todo, index) =>
    Row({
      "data-index": index,
      children: () => todo().title,
    }),
  { key: todo => todo.id },
);
06

Async + containment

Async state and error containment.

Resources expose loading, value, and error state. Boundaries replace failed owned subtrees.

Abortable resource

settled
Waiting for data
resource.ts
const users = resource(query, async (term, { signal }) => {
  const response = await fetch(`/api/users?q=${term}`, { signal });
  if (!response.ok) throw new Error("Request failed");
  return response.json();
});

Show(users.loading, () => Text({ children: "Loading…" }));
For(() => users.value() ?? [], user => UserRow(user));
Boundary armedFailure replaces this owned subtree.
07

Composition

Context follows ownership.

Providers inherit through the owner tree. Portals change DOM placement without changing context, cleanup, or error handling.

Current context value

Theme: paper

context.ts
const Theme = createContext("paper");

provide(Theme, "night", () => App());

function Toolbar() {
  const theme = useContext(Theme);
  return Text({ children: `Theme: ${theme}` });
}
portal.ts
Portal({
  target: document.body,
  children: () => Modal(),
});

// Modal keeps the context, error boundary,
// and cleanup scope of its logical parent.
08

Ownership

Disposal cleans owned work.

Effects, resources, listeners, branches, list records, and portals are disposed with their owner.

mount()root owner
provide()context owner
Show()branch owner
resource()effect + abort
Portal()physical DOM elsewhere
clock.ts
function Clock() {
  const time = signal(new Date());

  onMount(() => {
    const timer = setInterval(
      () => time.set(new Date()),
      1_000,
    );
    return () => clearInterval(timer);
  });

  return Text({
    children: () => time().toLocaleTimeString(),
  });
}
09

Public surface

API reference.

Core primitives for state, rendering, async work, and lifecycle.

Reactivity

signal(value)
Callable state with set, update, and peek.
computed(fn)
Lazy cached derived value.
effect(fn)
Immediate owned reaction; reruns in a microtask.
batch(fn)
Group signal writes into one flush.
untrack(fn)
Read without subscribing.

Rendering

mount(target, component)
Replace a target with one owned root range.
element(tag, props)
Create a native HTML or SVG element.
enhance(root, setup)
Attach owned behavior without replacing existing DOM.
component(fn, props)
Create an explicit nested lifecycle scope.
Show(when, yes, no)
Own exactly one conditional branch.
For(items, render, options)
Reconcile keyed DOM ranges.
Portal({ target, children })
Render elsewhere without changing ownership.

Static build

renderToString(component, props?)
Render deterministic initial HTML with @flub/static.
flubStatic({ pages })
Render selected Vite HTML entry points at build time.

Application

resource(source, fetcher)
Abortable async state with race protection.
ErrorBoundary(props)
Contain and reset failed subtrees.
createContext(default?)
Create an owner-inherited value channel.
provide(context, value, fn)
Add a context value and owner scope.
useContext(context)
Read the nearest provider.
onMount(fn) / onCleanup(fn)
Connect work to an owner lifecycle.

Elements

Div(props), Main(props), …
Typed factories for every modern HTML tag.
H({ h, …props })
Choose a native heading level at runtime.
Svg(tag, props)
Create any namespace-correct SVG element.
createElementFactory(tag)
Create a reusable, tag-specific native factory.

Layout components

Box / Flex / Row / Column
Small composable layout primitives.
Grid / Cluster
Grid tracks or a wrapping inline group.
Container / Center
Constrain content or center its children.
Spacer / AspectRatio
Fill flex space or preserve media geometry.

Interface components

Heading / Text / CodeBlock
Semantic typography without theme styles.
Field / *Input / SelectInput
Labeled controls with hint and error wiring.
Checkbox / Radio / Switch
Accessible labeled choice controls.
List / OrderedList
Keyed collections with semantic list items.
Disclosure / ModalDialog
Native interactive elements with reactive ownership.
Alert / Status
Live-region semantics with no visual policy.

Examples

Run the source examples.

View source ↗Open todo example