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.
First contact
Components return DOM nodes.
Call a component once. Use an accessor for values that should update.
npm install flub.js@nextThe counter creates its DOM once. Each click updates one text node.
Live signal
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),
}),
];
}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.
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”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
count.set(2)() => `Count: ${count()}`Text.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
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
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" });
}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/elementsTyped native factories
02
flub.js/componentsUnstyled composition
03
flub.jsEverything from one entry
HTML factory index
Every modern element.
112 / 112
Document + metadata
12Sections + headings
17Text semantics
38Forms + feedback
17Media + embeds
14Lists + tables
14All factories accept native ElementProps. H() selects a heading level at runtime; Svg() covers the SVG namespace.
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." }),
],
});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
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)
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),
});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 · AspectRatioCompose common CSS layouts while retaining native div props.
adds: gap · direction · wrap · columns · rows · maxWidth · gutter · ratio
- 02
Typography
Text · Heading · CodeBlock · Separator · VisuallyHiddenChoose semantic text and accessibility behavior without theme styles.
adds: as · level · code · language · orientation
- 03
Actions
Btn · Link · IconButtonSafer 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 · SelectInputConnect labels, descriptions, validation state, and native controls.
adds: label · hint · error · fieldProps · items
- 05
Choices + forms
Checkbox · Radio · Switch · ManagedFormLabel choice controls and collect repeated form values safely.
adds: label · hint · fields · onSubmit(values, data)
- 06
Collections
List · OrderedList · DescriptionList · BreadcrumbsRender keyed records inside the correct semantic container.
adds: items · render · key · fallback · term · details · separator
- 07
Disclosure + status
Disclosure · ModalDialog · Alert · StatusUse native disclosure, dialog, and live-region semantics.
adds: summary · open · onDismiss · modal
Dynamic structure
Conditional and keyed regions.
Show owns one branch. For moves keyed node ranges and disposes removed records.
Keyed identity lab
4 records- 01Signals
key:1 - 02Owners
key:2 - 03DOM ranges
key:3 - 04Cleanup
key:4
For(
todos,
(todo, index) =>
Row({
"data-index": index,
children: () => todo().title,
}),
{ key: todo => todo.id },
);Async + containment
Async state and error containment.
Resources expose loading, value, and error state. Boundaries replace failed owned subtrees.
Abortable resource
settledconst 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));Composition
Context follows ownership.
Providers inherit through the owner tree. Portals change DOM placement without changing context, cleanup, or error handling.
const Theme = createContext("paper");
provide(Theme, "night", () => App());
function Toolbar() {
const theme = useContext(Theme);
return Text({ children: `Theme: ${theme}` });
}Portal({
target: document.body,
children: () => Modal(),
});
// Modal keeps the context, error boundary,
// and cleanup scope of its logical parent.Ownership
Disposal cleans owned work.
Effects, resources, listeners, branches, list records, and portals are disposed with their owner.
mount()root ownerprovide()context ownerShow()branch ownerresource()effect + abortPortal()physical DOM elsewherefunction Clock() {
const time = signal(new Date());
onMount(() => {
const timer = setInterval(
() => time.set(new Date()),
1_000,
);
return () => clearInterval(timer);
});
return Text({
children: () => time().toLocaleTimeString(),
});
}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.