Installation
PulsePoint v2 is one file with zero dependencies. There is no package to install, no bundler to configure and no build step to run.
Self-hosted (recommended)
Download pp-reactive-v2.min.js from the
GitHub repository
and serve it from your static assets directory. Self-hosting pins your exact
version and keeps your app dependency-free at runtime.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script type="module">
import { ComponentInit as PP } from "/js/pp-reactive-v2.min.js";
PP.bootstrap();
</script>
</head>
<body>
<template pp-component="booking_form">
<div>
<!-- one root element per component, script included -->
</div>
</template>
</body>
</html>
Note the shape inside <body>: a
<template pp-component> as the direct child,
holding the component's single root element. Both halves matter, and they are
covered in the next two sections — the root is what a component is, and the
template is what keeps the browser from reacting to unrendered markup. Ship a page
without the template and typed inputs warn about their values, bound
src attributes fetch a broken URL, and the
component's script runs before the runtime exists.
Importing the runtime makes the global pp object available to component
scripts, but it does not start the page. The module import calls
PP.bootstrap() exactly once to materialize the template boundaries and start reactivity.
CDN
For prototypes and single-file demos:
<script type="module">
import { ComponentInit as PP } from "https://pulse-point-cdn.pages.dev/pp-reactive-v2.min.js";
PP.bootstrap();
</script>
One root element per component
PulsePoint follows the React component pattern: a component is exactly one root
element. That root carries the
pp-component attribute, everything the
component renders lives inside it, and so does its
<script>. A plain container
<div> is the usual choice, but any element
works — a <form>,
<section> or
<article> is a root just as well.
<div pp-component="booking_form">
<form>
<input type="date" value="{startDate}">
<input type="number" value="{guests}" min="1">
<button type="submit">Book</button>
</form>
<p hidden="{!guests}">Booking {guests} guest(s) for {startDate}.</p>
<script>
const [startDate, setStartDate] = pp.state("2026-01-15");
const [guests, setGuests] = pp.state(2);
</script>
</div>
Two roots side by side are two components, each needing its own
pp-component id and its own script. If a
component's markup wants siblings at the top level, give them a wrapper element —
the same move you would make in React.
Deferring the root with <template pp-component>
A root can be handed to the browser inside a
<template> instead of being written directly
into the page. This changes nothing about the component — it is the same single
root, just deferred:
<template pp-component="booking_form">
<div>
<form>
<input type="date" value="{startDate}">
<input type="number" value="{guests}" min="1">
<button type="submit">Book</button>
</form>
<p hidden="{!guests}">Booking {guests} guest(s) for {startDate}.</p>
<script>
const [startDate, setStartDate] = pp.state("2026-01-15");
const [guests, setGuests] = pp.state(2);
</script>
</div>
</template>
A <template> element's contents are
inert. The browser parses them, but they never enter the live document, so
nothing inside is validated, fetched, painted or executed until PulsePoint says so.
On mount the runtime clones the content, copies the template's attributes onto its
first element child, and replaces the template with that child — producing exactly
the <div pp-component="booking_form"> from
the previous section. The wrapper leaves no trace in the final DOM, which is also
why it holds one element: only the first child becomes the root.
A backend integration should do this for you. Caspian wraps every outermost component root at render time, so you author the plain single-root form and the deferred form is what ships — see Backend Integration. In a hand-written page there is no server to do it, so you write the wrapper yourself; Plain HTML covers that setup.
What deferral prevents
An undeferred root is still a valid component, and on a page of text and buttons
you will never notice the difference. It starts to matter when the markup carries
values the browser tries to interpret at parse time, because until PulsePoint runs
those attributes still hold raw
{expression} text. The form above,
written directly into the body, loads like this:
The specified value "{startDate}" does not conform to the required format, "yyyy-MM-dd".
The specified value "{guests}" cannot be parsed, or is out of range.
Uncaught ReferenceError: pp is not defined
-
Typed inputs warn and discard the value.
date,number,colorandrangefields validate theirvaluethe moment they are parsed. A placeholder is not a valid date, so the browser logs a warning and throws the value away. Acolorfield adds its own: the format is "#rrggbb" where rr, gg, bb are two-digit hexadecimal numbers. -
URL attributes fire a real request.
<img src="{avatarUrl}">makes the browser fetch the literal path/%7BavatarUrl%7D, which fails and leaves a broken image until compilation replaces it. The same applies tosrcset,posterandhref. -
SVG geometry errors outright. A bound
dattribute raisesExpected moveto path command ('M' or 'm');points,viewBoxandtransformare rejected the same way. -
The component's own
<script>runs too early. The runtime loads as a deferred ES module, so a script sitting in the live document executes beforeppexists — in global scope, where it does not belong. Inside a template it is never run by the browser at all; PulsePoint captures the source and evaluates it once, in component scope. - Raw placeholders flash on screen before the first render replaces them.
Deferred, the same markup loads with an empty console, no wasted request, and
every field already holding its real value. One detail to keep straight when you
write the wrapper by hand: pp-component moves onto
the <template>, so it is not repeated on the
root inside.
The opacity: 0 alternative
You may also see <body style="opacity: 0"> used
as a flash guard: pp.mount() hides the body while it
compiles and restores your original inline style on the next frame. It is a
reasonable fallback for markup you cannot defer, but it only hides the flash — the
input warnings, the failed request and the early script all still happen behind the
invisible body. Prefer deferral; reach for
opacity: 0 on top of it, not instead of it.
Verify the install
Drop this into a served page. It works with or without a backend that defers roots — Caspian skips a root that is already a <template> rather than wrapping it twice:
<body>
<template pp-component="smoke_test">
<div>
<p>{message}</p>
<button onclick="setMessage('It works!')">Click me</button>
<script>
const [message, setMessage] = pp.state("PulsePoint is mounted.");
</script>
</div>
</template>
</body>
If clicking the button swaps the message, the runtime is mounted and reactive. If
nothing at all appears, the template was never materialized — the script tag is
missing or its path is wrong, since an unmounted
<template> renders no content rather than
literal braces. And if your own markup logs
ReferenceError: pp is not defined while this
sample works, that root is not being deferred. Check the console for
[PP-ERROR] messages either way.
Requirements
- Any evergreen browser (the runtime uses standard DOM APIs,
fetchandWebSocket). - Any web server. Static hosting works for read-only pages; add the RPC contract when components need to call the server.