Components
A component is a server-rendered HTML region marked pp-component="unique_id", owning one <script> inside its root. Composition, props and context all cross these boundaries as plain HTML.
Boundaries and nesting
<div pp-component="product_list_1">
<h2>Products</h2>
<!-- A nested component: its attributes become pp.props inside it -->
<div pp-component="product_card_1" title="{selected.title}" on-buy="{buy}">
…card markup…
<script>
// Inside the card:
// pp.props.title — evaluated in the PARENT scope, keeps its real type
// pp.props.onBuy — kebab-case attribute on-buy arrives camelCased
</script>
</div>
<script>
const [selected, setSelected] = pp.state({ title: "Keyboard" });
const buy = () => pp.rpc("buy", { id: 1 });
</script>
</div>
- The server generates the unique id — any scheme works, it just must not repeat on the page.
- One root element per component is the default shape; the script lives inside that root. Two more root shapes exist: a composition root (another component as the root) and a multi-root fragment.
- Each instance gets its own hook state. The script runs once per instance.
Props
Attributes on a nested component's root become pp.props in its script:
| Attribute | What the child sees |
|---|---|
title="{item.title}" |
Evaluated in the parent's scope; keeps its real type (object, array, function…). |
count="0" |
A literal server value arrives as the string "0" — compare accordingly. |
disabled |
A valueless attribute becomes true. |
on-select="{choose}" |
Kebab-case arrives camelCased: pp.props.onSelect. |
class |
JS reserved words are dropped from pp.props — use another name. |
Prop identity decides whether a child re-renders: pass stable functions
(pp.callback) and stable objects (pp.memo or state) when a child
is expensive. In v2, a mounted child boundary is reconciled by its
attributes, not by re-parsing its markup — the parent's render skips
the child's body entirely when props are unchanged.
Children (slot content)
Markup a parent passes into a child component is rendered inside the child's
boundary, wrapped in <template pp-owner="parent_id">. The wrapper
declares whose scope the content reads: expressions, event handlers and
pp-ref bindings inside it resolve in the owner's script, not the
child's — exactly like React children. The runtime replaces the template in
place with the rendered content, so the child decides where children appear
by where the server emits the wrapper:
<div pp-component="page_1">
<!-- The markup the parent passes into card_1 travels INSIDE the child,
wrapped in a template that names its owner. -->
<div pp-component="card_1" title="Team">
<template pp-owner="page_1">
<p>{memberCount} members</p>
<button onclick="{invite()}">Invite</button>
</template>
</div>
<script>
const [memberCount, setMemberCount] = pp.state(3);
const invite = () => setMemberCount(memberCount + 1);
</script>
</div>
- The owner id resolves like any component reference; the alias
pp-owner="app"refers to the page's root component instance. - When the owner re-renders, its slot content re-renders with it — the child does not need to know or care.
- A plain
<script>inside slot content is treated as the owner's component script, never rendered as markup — that is what powers composition roots, below.
Composition components (another component as the root)
A component's root may be another component — the React pattern of a
wrapper component returning <Card>...</Card> as its root. The rendered
shape has three parts: a host element marked with the composition component's id and
style="display: contents" (so it adds nothing to layout), the child
component's boundary as the host's only element child, and the composition
component's own <script> travelling as slot content — inside the child,
wrapped in <template pp-owner="composition_id">:
<!-- ConfirmButton's root IS another component (button_1).
The host contributes no box of its own (display: contents); its
<script> rides along as slot content, wrapped in template[pp-owner]. -->
<div pp-component="confirm_button_1" style="display: contents"
label="Delete account">
<button pp-component="button_1" variant="destructive" onclick="{confirm()}">
<template pp-owner="confirm_button_1">
<script>
const { label = "Confirm" } = pp.props;
const [armed, setArmed] = pp.state(false);
const confirm = () => setArmed(!armed);
</script>
{armed ? "Are you sure?" : label}
</template>
</button>
</div>
- The runtime recovers the projected script and runs it in the composition component's scope, so its state and handlers are exactly where its slot content expects them.
- Attributes on the host become the composition component's
pp.props; attributes on the inner boundary become the child's. - Ref forwarding: a host carrying
pp-ref="{someRef}"pluspp-ref-forward="true"resolves the ref through thedisplay: contentschain to the first concrete component root — PulsePoint's equivalent offorwardRef. The chain must present exactly one child component root at each hop.
Fragments (multi-root components)
A component whose top level is a run of siblings is a fragment. Instead of an
element boundary, the server frames the run with a comment pair — legal in every
content context, including <tbody> and <select> where a wrapper
element would be foster-parented out by the HTML parser:
<!-- A multi-root component: siblings framed by a comment pair.
No wrapper element ever enters layout. -->
<!--pp:quick_tally_1-->
<button onclick="setCount(count + 1)">Tally</button>
<p>Total: {count}</p>
<script>
const [count, setCount] = pp.state(0);
</script>
<!--/pp-->
- At mount the runtime converts each pair into a live
<pp-fragment style="display: contents">element carrying the id — identity, scope, events and re-renders anchor to it while it stays out of layout. Fragments nest; a close marker pairs with the nearest unclosed open. - Under
<table>/<select>parents the pair stays as comments: the grouping renders, but the fragment owns no identity there — give a stateful fragment a context an element could also live in. - A fragment has no root element, so it cannot receive props or a
pp-ref— use a single-root component when the parent needs to pass either. The id afterpp:may be empty for a grouping that owns no identity. - These markers are the server's output shape. Never hand-type
<pp-fragment>in source — it is what the runtime materializes, not what you author.
Context
Context flows values to any depth without prop threading. Create a token with
pp.createContext, provide it with a lowercase
<token.provider value="{v}"> element, consume it with
pp.context(token):
<div pp-component="theme_root">
<theme.provider value="{themeValue}">
<div pp-component="toolbar_1">
<button pp-style="{'background: ' + currentTheme.accent}">Save</button>
<script>
const currentTheme = pp.context(theme);
</script>
</div>
</theme.provider>
<script>
const theme = pp.createContext({ accent: "#0af" });
// Give providers a STABLE value: state or a memo, not a fresh object per render.
const [themeValue, setThemeValue] = pp.state({ accent: "#0af" });
</script>
</div>
Error boundaries
A component that calls pp.errorBoundary() catches render-time throws from
its subtree instead of letting them reach the console:
<div pp-component="safe_zone">
<div hidden="{!error}">
<p>Something went wrong: {error?.message}</p>
<button onclick="reset()">Try again</button>
</div>
<div hidden="{error}">
<!-- children that might throw during render -->
</div>
<script>
const [error, reset] = pp.errorBoundary();
</script>
</div>
A boundary also catches its own render and effect throws, stays in the error state
until reset() is called, stops catching after five errors without a reset,
and does not cover event handlers. Details: Advanced Hooks.
Server-side composition
How components are authored is your backend's business: a PHP include, a
Jinja macro, a Go template, a Python function returning markup. PulsePoint only
sees the rendered result — regions marked pp-component. This is what
makes the engine backend-agnostic: composition is a server concern, reactivity
is a browser concern. See Backend Integration
for per-language patterns, including layouts that wrap a page component.