flub is a tiny DOM microframework. Components construct real DOM once; signals update only the text, properties, styles, and keyed regions that depend on them. There is no virtual DOM, compiler, or browser-runtime dependency.
v2 is currently an alpha. The v1
setStatererendering API has been removed because replacing component roots cannot preserve DOM or component identity.
npm install flub.js@nextimport { Btn, Column, Row, Text, mount, signal } from "flub.js";
mount(document.body, Counter);
function Counter() {
const count = signal(0);
return Column({
gap: 16,
children: [
Text({ as: "h1", children: () => `Count: ${count()}` }),
Row({
gap: 8,
children: [
Btn({ children: "−", onClick: () => count.update((n) => n - 1) }),
Btn({ children: "+", onClick: () => count.update((n) => n + 1) }),
],
}),
],
});
}mount() returns a disposal function that removes the mounted range and cleans
up every owned effect and event listener.
Components are ordinary functions and are called once. Call nested components normally:
Div({ children: Heading() });A function in a DOM value position is a reactive accessor:
const name = signal("Ada");
Text({ children: () => `Hello, ${name()}` });
Input({ value: name, disabled: () => name() === "" });This explicit distinction lets flub update a text node or DOM property directly instead of rerunning the component and diffing a second tree.
const first = signal("Ada");
const last = signal("Lovelace");
const fullName = computed(() => `${first()} ${last()}`);
effect(() => console.log(fullName()));
batch(() => {
first.set("Grace");
last.set("Hopper");
});signal(value)creates a callable accessor with.set()and.update().computed(fn)is lazy and cached.effect(fn)runs immediately, then once per asynchronous update batch.batch(fn)groups related writes.untrack(fn)reads signals without adding dependencies.
Use Show and For when the DOM structure itself changes:
Show(
() => session() !== null,
() => Dashboard(),
() => Login(),
);
For(
todos,
(todo, index) =>
Div({
"data-index": index,
children: () => todo().title,
}),
{ key: (todo) => todo.id },
);Show disposes the inactive branch. For moves existing keyed nodes during a
reorder and gives each item its own reactive value and cleanup scope.
ErrorBoundary contains failures from components, reactive DOM bindings,
effects, and lifecycle hooks. Resetting disposes the failed subtree and creates
a fresh owner:
ErrorBoundary({
children: () => Dashboard(),
fallback: (error, reset) =>
Btn({ children: `Retry: ${error.message}`, onClick: reset }),
});resource(source, fetcher) tracks loading, errors, and the latest value. It
aborts the previous request when its source changes or its owner is disposed,
and ignores late responses:
const users = resource(userQuery, async (query, { signal }) => {
const response = await fetch(`/api/users?q=${query}`, { signal });
if (!response.ok) throw new Error(`Request failed (${response.status})`);
return response.json();
});
Show(users.loading, () => Text({ children: "Loading…" }));
For(
() => users.value() ?? [],
(user) => Text({ children: user().name }),
);Reading resource.value() throws a stored request error, so a reactive consumer
automatically enters its nearest ErrorBoundary. Read resource.error() to
build an inline error state instead. Resources also provide .refetch(),
.mutate(value), and .dispose().
Scheduled errors without a boundary are rethrown in a microtask by default.
Hosts can replace that behavior with setUnhandledErrorHandler(handler) for
logging or crash reporting; the function returns a restore callback.
Context values follow the owner tree and avoid global singletons or prop drilling. Providers create their own cleanup scope, and the nearest provider wins:
const Theme = createContext("paper");
provide(Theme, "night", () => App());
function Toolbar() {
const theme = useContext(Theme);
return Text({ children: `Theme: ${theme}` });
}Portal renders a factory into another element or shadow root without changing
its logical owner. Portalled components retain context, lifecycle cleanup, and
their nearest error boundary:
Portal({
target: document.body,
children: () => Modal(),
});function Clock() {
const time = signal(new Date());
const timer = setInterval(() => time.set(new Date()), 1000);
onMount(() => console.log("mounted"));
onCleanup(() => clearInterval(timer));
return Text({ children: () => time().toLocaleTimeString() });
}Use component(Clock) when a nested component needs a distinct lifecycle scope.
Dynamic Show branches and For items already receive their own scopes.
element(tag, props) and the element helpers use native DOM semantics:
- Boolean properties such as
disabledandcheckedare not stringified. value,selected, and other DOM properties update as properties.aria-*,data-*, and unknown values remain attributes.classsupports strings, arrays, and conditional objects.stylesupports strings and objects; numeric dimensional values use pixels.onClick,onclick, andon:custom-eventuseaddEventListener.- String event handlers and raw
innerHTMLare rejected. Svg(tag, props)creates namespace-correct SVG elements.
Every modern HTML tag has a typed PascalCase factory, from A and Article
through Table, Textarea, and Video. LinkElement, MapElement, and
ObjectElement use explicit names to avoid ambiguous imports. Build a factory
for a tag once when authoring your own primitive:
import { createElementFactory } from "flub.js/elements";
const Card = createElementFactory("article");
Card({ class: "card", children: "Native, typed, and reusable." });The component entry point is an unstyled accessibility and composition layer:
- Layout:
Box,Flex,Row,Column,Grid,Cluster,Container,Center,Spacer, andAspectRatio. - Typography:
Text,Heading,CodeBlock,Separator, andVisuallyHidden. - Forms:
Btn,IconButton,Field, common typed inputs,TextareaInput,SelectInput,Checkbox,Radio,Switch, andManagedForm. - Collections:
List,OrderedList,DescriptionList, andBreadcrumbs. - Disclosure:
Disclosure,ModalDialog,Alert, andStatus.
Components add native semantics, ownership, and useful accessibility wiring. They intentionally ship without theme CSS, so application styles remain in control.
Use attr:name or prop:name to explicitly select attribute or property behavior
for unusual custom elements.
import { signal, mount } from "flub.js/core";
import { Row, Text } from "flub.js/components";
import { Article, Button } from "flub.js/elements";The package is ESM-only and includes TypeScript declarations and source maps. Its intended scope is client-rendered widgets, islands, internal tools, and small-to-medium browser applications. Routing, full hydration, and a JSX compiler are not v2 goals.
Static HTML is an opt-in build feature. Existing applications keep using
mount() with no configuration or additional dependency. To render selected
pages during a Vite build, install the companion package:
npm install -D @flub/static// vite.config.ts
import { flubStatic } from "@flub/static/vite";
export default {
plugins: [
flubStatic({
pages: { "index.html": "src/page.ts" },
}),
],
};Place <!--flub-static--> in the HTML body and default-export a component from
src/page.ts. The build writes its initial DOM into the page. Reactive values,
Show, and For render their initial state; effects, refs, and event listeners
remain browser-only.
Static output does not require hydration. Use enhance(root, setup) to attach
owned listeners to existing markup, or mount() small interactive widgets as
independent islands. Disposing an enhancement runs its registered lifecycle
cleanup without replacing the static DOM.
npm install
npx playwright install chromium
npm run dev
npm run test:guide
npm run test:conformance
npm run checkThe complete flub-powered field guide is available at /examples/guide/. It is
written in TypeScript, statically rendered with @flub/static, and progressively
enhanced with flub. It includes a filterable element catalog, live component
examples, and component-specific prop references.
npm run check runs strict typechecking across the library, tests, and
TypeScript examples, plus linting, formatting verification, unit, browser, and
guide smoke tests, package-contract validation, and the bundle-size budget.
See the design contract for the architectural boundaries. See the conformance contract for lifecycle workloads, garbage-collection checks, and committed performance budgets.