A radial interface for monitoring and controlling systems.
Pulse shows each system as a circular object with layers for its readings and controls. In a home, for example, the climate system can have a thermostat in front, with its mode and fan controls behind it. You drag the ring to change the temperature or tap another layer to bring it forward.
When an alert appears, Pulse brings that layer to the front. Once the alert is resolved, the layer moves behind the active one. You can see what needs attention while keeping the other readings and controls within reach.
This repository is the core, @zumer/pulse: the object, the board and the
contract a source fills, with a demo of simulated data. The apps that connect
a real source are separate repositories (see Sources).
The site, with the full documentation, is zumerlab.com/pulse. Rings and layers are Orbit; the zoom between levels is Zumly.
- Layers show priority. The active channel sits in front. An alert brings its layer forward; a more serious alert takes priority. When resolved, the layer moves back behind the active one.
- A few layers stay visible. The default is three on a desktop and two on a phone. Other channels remain accessible by name. Only the front layer has a full disc; the layers behind it show a ring and a label.
- Alerts mark the affected part. A heavier stroke, color and a wave draw attention to a ring, segment or label. The disc stays unchanged.
- Controls sit on the ring. Drag a band to adjust a value, tap a segment to toggle a device, or choose a mode by name. Actions that need a code open a keypad. A separate detail view opens when there is more to show, such as history or a camera feed.
- Commands wait for confirmation. The control pulses while a command is pending and settles when the source reports back. A rejected command leaves a red outline and a Retry option.
![]() |
![]() |
| Drag the front ring to adjust the thermostat. | A battery alert moves to the front, with an action below. |
![]() |
![]() |
| The same home by room, each area an object. | Settings for this screen, saved in this browser. |
Read about the design decisions, object structure, components and board views.
The object is the same whatever feeds it. Each source is an app built on the core, in its own repository. The first is the home: Pulse for the home runs on Home Assistant and on openHAB, as a card, a dashboard, a section, a page the server itself serves, or a standalone app. Your own source below is how to write another.
npm install @zumer/pulse @zumer/orbit zumlyThree stylesheets and two runtimes (Orbit's, imported here; Zumly's, which the core brings), then the board:
import 'zumly/style.css';
import '@zumer/orbit/style';
import '@zumer/orbit';
import '@zumer/pulse/style.css';
import { mountBoard } from '@zumer/pulse';
const board = mountBoard(canvas, {
host,
settings: 'my-settings',
strings: { things: 'Sensors' },
});
// When removing the interface:
board.destroy();Here, canvas is the element inside your page's shell and host implements
PulseHost. For the page structure and a complete adapter, follow
In your project.
A host supplies systems, reports changes and runs actions. It also provides
names and state labels for the data. Optional methods supply history, camera
streams, statistics and groups such as rooms. The PulseHost interface is
defined in src/model.ts.
Each system has channels. A channel returns a Reading, or null when it
has nothing to show. Readings describe one of three ring types:
| Kind | Display and interaction |
|---|---|
'p' |
Progress band; an optional dial or range makes it adjustable |
's' |
Segments representing switches |
'c' |
Named options with one selected |
Readings also include the main value, a label and the entity ids they depend
on. They can add alerts and detail content such as history, an image, a color
control or a longer list. A reading with a map opens a detail where the map
is big, follows the reading live, zooms with the wheel, a pinch or its
buttons and moves with a drag or the arrows; a link adds a button out to the
thing elsewhere. A picture with a video plays muted over its still, in the
face and in the detail: a live HLS stream or a video file. A segment or a row
can name a thing with its own object (to): with objectOf(id) the host
makes it, and tapping the row zooms into it. A reading's plots are a radar,
its globe the disc as the Earth, its map a window on a tiled map.
Actions belong to the host. The board sends them through act, tracks the
ids they affect and waits for reported changes. An action that requires a
code opens a keypad first; one that asks for words opens an input instead.
Start with In your project for the setup.
The Tutorial
builds one from zero, with screens: a simulated greenhouse as three objects,
whose code is in demo/greenhouse/. The component gallery at
demo/elements.html shows individual components with sample data under each
of the seven built-in themes.
mountBoard(canvas, options) returns a handle for updating and removing the
interface. Options include available systems and groups, initial view,
starting system, appearance, layer counts, status text and translated strings.
Browser preferences are saved under the key supplied as settings.
Set settings: false to disable the settings panel and saved preferences.
Set round: true for a round display: the canvas becomes the circle
inscribed in its container, every view keeps inside it with the object in the
center, and the gear is left out. Set knob: true to handle the board with one
knob: the arrow keys or the wheel turn, Enter or Space presses, and a long
press or Escape goes back. Turning walks what the view offers, pressing opens
or acts, and a dial or a list takes the turn while it is held. An encoder that
reaches the page some other way calls board.knob.turn(1),
board.knob.press() and board.knob.back(). Round is laid out for displays
of 720 px and up.
Use setHost to change data sources, setSettings to update display choices,
repaint to refresh the views and destroy to release the board's resources.
The ready promise tracks initialization. See
src/board/types.ts for the full options and handle types.
The repository carries an agent skill, skills/pulse/:
what Pulse is, when to use it, a tested example, the contract and the common
mistakes, for a coding agent that has never seen Pulse. Install it in Claude
Code with /plugin install pulse --marketplace zumerlab/orbit, with npx skills add zumerlab/pulse, or by
copying the folder into .claude/skills/pulse/. The other ways, and what the
skill asks of agents, are on the site.
| Directory | Contents |
|---|---|
| src/ | The package: the contract, the board, the radial components, the themes, the strings, the stylesheet |
| demo/ | Simulated systems, a component gallery and the tutorial's greenhouse; npm run dev serves them |
tests/ |
Playwright tests, run against the demo |
| docs/ | Website, component reference and design documentation |
scripts/ |
land.mjs, which generates the globe's coasts |
Inside src/:
| Module | Responsibility |
|---|---|
| model.ts | Host, system, channel, reading and action contracts |
| board/ | Board state, settings, commands, views and lifecycle |
| radial/ | Visual components, gestures, dimensions and Zumly setup |
| themes.ts | Seven themes and helpers for applying colors |
| tune.ts | Layout parameters exposed by the Tuning tab |
| history.ts | Hourly aggregation and state-change helpers |
| i18n.ts | Shared interface text and locale formatting |
| style.css | Entry stylesheet, importing the modules in styles/ |
Inside board/, mount.ts handles setup and cleanup, context.ts defines the
shared state, and separate modules handle settings, actions, drawing, history,
dials and the settings panel. views/ contains system, detail and change
views, with a module for each board layout.
Inside radial/, components are grouped by family: rings, objects, keypads,
color controls, time displays and lists. You can use these independently of
mountBoard; the demo does this.
The base stylesheet uses Graphite. Other built-in themes are Paper, Deep,
Slate, Mist, Fog and Night. applyTheme selects one; applyColors supplies
custom colors.
Requires Node.js 22.18+ or 24+ and npm.
npm ci
npm run dev # the demo, http://127.0.0.1:5173; the component gallery at /elements.html; the tutorial's greenhouse at /greenhouse/
npm test # lint, types, the build, then Playwright over the demo
npm run compile # the package into dist/: index.js, its types, the stylesheetCommits follow Conventional Commits
(feat:, fix:, docs:, refactor:, chore:): the version bump and the
changelog read them.
npm test
npm run bump # suggests the next version from the commits, tags it, writes CHANGELOG.md
npm run release # commits the changelog and pushes with the tag
npm publish # compiles firstThe globe's coasts (src/radial/land.ts) are Natural Earth's land at
1:110,000,000, public domain, taken from the
world-atlas package and decimated
by scripts/land.mjs. The map face loads tiles from
OpenStreetMap by default
(© OpenStreetMap contributors), whose tile policy asks for light use; a host
passes its own tiles template for anything heavier.
Everything in the repository is in English: code, comments, commits and docs.
Shared interface strings live in src/i18n.ts, each app adds its own in
strings.ts; numbers and times follow the language the source reports.
Translations can be added through those dictionaries.




