Skip to content

Author without JSX

Read as Markdown

createElement(type, props, ...children) is the complete non-JSX API for Pibbl component values. It uses the same branded element protocol, component identity, child traversal, style, hooks, events, focus policies, scheduling, and disposal as JSX. You do not need any JSX compiler configuration to use this guide.

Terminal window
pnpm add @pibbl/core
import {
FocusManagement,
Fragment,
Group,
KeyboardNavigation,
Rectangle,
Text,
pibbl,
createElement,
useSignal,
type PibblController,
type PibblNode,
} from "@pibbl/core";
interface PosterProps {
initialMessage: string;
}
function Poster({ initialMessage }: PosterProps): PibblNode {
const message = useSignal(initialMessage);
const cards = ["A", "B"].map((label, index) =>
createElement(
Group,
{ key: label, children: null },
createElement(Rectangle, {
style: {
left: 32 + index * 100,
top: 100,
width: 84,
height: 56,
fill: "#f7c948",
cursor: "pointer",
},
onClick: () => message.set(label),
}),
createElement(
Text,
{ pointerEvents: "none", style: { left: 64 + index * 100, top: 116 } },
label,
),
),
);
return createElement(
FocusManagement,
null,
createElement(
KeyboardNavigation,
{ mode: "directional", children: null },
createElement(
Group,
null,
createElement(Rectangle, {
style: { width: 240, height: 180, fill: "#1f4bd8" },
}),
createElement(
Text,
{
pointerEvents: "none",
style: { left: 24, top: 28, fill: "white" },
},
"Selected: ",
message,
),
createElement(Fragment, { key: "choices", children: null }, cards),
),
),
);
}
export function mount(canvas: HTMLCanvasElement): PibblController {
return pibbl(canvas, createElement(Poster, { initialMessage: "None" }));
}
export function attach(canvas: HTMLCanvasElement): () => void {
const controller = mount(canvas);
return () => controller.dispose();
}

Call controller.dispose() when the owner removes the Canvas or no longer needs the scene. Disposal is safe to repeat.

createElement(type, props, ...children);
  • type is a Pibbl component value or ordinary synchronous component function.
  • props is the component’s props object or null.
  • With no variadic children, an explicitly supplied props.children is preserved.
  • One variadic child becomes children; several become an array. Variadic children override props.children.
  • For a dynamic list, pass the array as one third argument: createElement(Group, null, items.map(makeItem)).
  • key may be a string or number, is normalized to a string, and is removed from delivered props.
  • The constructor copies the top-level props object. Development builds shallow-freeze synthetic props and the element, but nested author objects are not deep-frozen.

Directly calling Rectangle(props) or another platform component does not create an element and throws a targeted diagnostic. Always use createElement(Rectangle, props).

Group, Clip, Layer, Absolute, Overlay, Flow, Flex, and Grid accept recursive synchronous node input. Arrays and finite iterables flatten; booleans, null, undefined, the empty string, and zero are empty. Non-empty raw strings and nonzero numbers outside Text are errors.

Pass textual values to Text as constructor children:

createElement(Text, { style: { left: 20, top: 20 } }, "Count: ", 0);

Zero renders inside Text. Text children may be nested arrays or finite iterables of strings, numbers, and empty values. Elements are invalid inside textual content.

Fragment is available for an explicit keyed identity boundary:

import { Fragment, createElement, type PibblNode } from "@pibbl/core";
function keyedSection(section: { id: string; elements: PibblNode }) {
return createElement(
Fragment,
{ key: section.id, children: null },
section.elements,
);
}

FocusManagement and KeyboardNavigation are component values here too. Each policy requires exactly one Pibbl element after empty values are removed; nest them as shown in the full example. They do not create paint, layout, or an extra identity boundary.

Hooks run only when Pibbl later invokes the component during its synchronous scheduled traversal. Calling the component directly is not a substitute for constructing an element. All hook-order, identity, failure, and ownership rules in the lifecycle guide apply unchanged.

The executable Studio lesson is create-element-authoring.example.tsx.

defineThreeLayer() returns an ordinary Pibbl component value, so it can be passed to createElement() just like a built-in. The Three scene itself still uses normal Three constructors rather than Pibbl elements:

const scene = createElement(ProductScene, {
rotation: 0.5,
missBehavior: "pass-through",
style: { width: 320, height: 180 },
});

See Use Three.js inside Pibbl for the layer definition, picking, scheduling, and resource-ownership contracts.

Open the interactive workbench

Read the Authoring, signals, and lifecycle companion for ownership, adaptation, failure modes, and verification. Agent start provides the version-selection workflow.

Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.