Building custom UIs
Four tiers — from drop-in <CircuitEmbed /> to full composition with useCircuitSimulator + CircuitCanvas.
<CircuitEmbed /> is the default for putting an interactive circuit
on a page. When you need more — a sibling that reacts to switch
toggles, a custom HUD, a debugger driving the sim — the same
primitives are available; you compose them yourself.
The simulator runs in pure JavaScript inside the sandbox iframe, independent of any rendering. Each tier below is a different way for your React UI to subscribe to its state and write inputs back. Pick the highest one that gives you what you need.
Tier 1 — Drop-in <CircuitEmbed />
The default. One component, sensible defaults, identical look across every page on simten.dev. Use this unless you need something it doesn't give you.
import { CircuitEmbed } from '@simten/embed'
<CircuitEmbed
circuit={MyCircuit}
title="Half adder"
description="Two switches → XOR for the sum, AND for the carry."
/>No state out, no imperative control — just a self-contained interactive embed.
Every page in Examples uses this pattern, as do
most inline circuits across the /blog/* posts.
Tier 2 — Read sim state with onPortValuesChange
When you want a sibling component to react to the embed's live state
(e.g. a truth-table highlight that follows the user's switch toggles,
or an external readout of a specific port value), pass an
onPortValuesChange callback. The embed fires it once when the sim
first settles, then again on every subsequent settled change.
import { useState } from 'react'
import { CircuitEmbed } from '@simten/embed'
import type { FlatPortValueMap } from '@simten/core/simulator'
import { TruthTable, computeActiveRow } from '@/components/TruthTable'
function HalfAdderWithLiveTable() {
const [portValues, setPortValues] = useState<FlatPortValueMap | null>(null)
const activeRow = computeActiveRow(portValues, COLUMNS, ROWS)
return (
<>
<CircuitEmbed
circuit={HalfAdder}
title="Half adder"
onPortValuesChange={setPortValues}
/>
<TruthTable columns={COLUMNS} rows={ROWS} highlightRow={activeRow} />
</>
)
}Live demo — toggle the switches:
The callback is captured in a ref inside the embed, so inline
functions like onPortValuesChange={(pv) => setX(pv)} are safe — they
won't cause spurious re-fires across parent renders.
Firing semantics
A few things worth knowing about when the callback fires:
- First fire — once the embed's internal simulator becomes ready
and port values have first settled. Before that, the callback
doesn't fire and your
useState<FlatPortValueMap | null>(null)stays atnullfor one paint. Plan for that initial frame in your rendering (e.g.computeActiveRowreturnsundefinedcleanly when port values are absent). - Subsequent fires — only on settled (post-propagation) states. The simulator never exposes intermediate propagation states, so there's no risk of flicker from mid-propagation values.
- Reference identity — the port-values
Mapis not guaranteed to be a stable reference across no-op ticks. Sequential auto-run loops can emit newMapinstances with unchanged contents. If you're doing expensive derived work, memoize on the actual values you care about rather than on theMapreference itself.
The /learn/adders page uses this for its live truth
tables — toggle switches on the half-adder or full-adder, watch the
matching row in the table light up.
Tier 3 — Write into the sim with the imperative handle
When a sibling needs to drive the embed (e.g. clicking a row in a
truth table should set the canvas switches to that combination), use
the existing ref handle. No new API required.
import { useRef } from 'react'
import { CircuitEmbed, type CircuitEmbedHandle } from '@simten/embed'
function HalfAdderDrivenByTable() {
const embedRef = useRef<CircuitEmbedHandle>(null)
return (
<>
<CircuitEmbed ref={embedRef} circuit={HalfAdder} />
<TruthTable
...
onRowClick={(row) => {
embedRef.current?.setNodeValue('a', row[0])
embedRef.current?.setNodeValue('b', row[1])
}}
/>
</>
)
}Pair tier 2 (read out) and tier 3 (write in) to get bidirectional binding between the embed and any sibling UI you write.
No page on simten.dev currently needs tier 3 — the API is available but nothing's using it yet. The most likely first consumer would be a clickable truth table that drives the embed's switches from the table side.
Tier 4 — Full composition
When you want to break out of the embed's chrome entirely and build
your own frontend on top of the simulator, drop down to the underlying
primitives. useCircuitSimulator gives you the live state and the
actions; render whatever React you want with them. The circuit becomes
your engine, your component is the UI.
import { useCircuitSimulator } from '@simten/embed'
import { TruthTable, computeActiveRow } from '@/components/TruthTable'
function HalfAdderWithCustomControls() {
const sim = useCircuitSimulator(HalfAdder, { autoHarness: true })
const a = readPortBit(sim.portValues, 'a')
const b = readPortBit(sim.portValues, 'b')
const sum = readPortBit(sim.portValues, 'sum')
const carry = readPortBit(sim.portValues, 'carry')
return (
<>
{/* Custom toggle buttons drive sim.setNodeValue */}
<ToggleControl value={a} onToggle={() => sim.setNodeValue('a', a ? 0 : 1)} />
<ToggleControl value={b} onToggle={() => sim.setNodeValue('b', b ? 0 : 1)} />
{/* Styled LED readouts pull from sim.portValues */}
<LedReadout lit={sum === 1} label="sum" />
<LedReadout lit={carry === 1} label="carry" />
<TruthTable
columns={COLUMNS}
rows={ROWS}
highlightRow={computeActiveRow(sim.portValues, COLUMNS, ROWS)}
/>
</>
)
}Live demo — same HalfAdder circuit as tier 2, but rendered as
custom toggle buttons and LED indicators instead of a schematic. No
CircuitCanvas anywhere in this tree:
The single useCircuitSimulator(HalfAdder) call creates one simulator
instance. Custom UI reads from sim.portValues and writes via
sim.setNodeValue; the truth table reads from the same map. One sim,
many consumers, all of them yours.
The simulator is still doing real work — compiling, propagating, exposing port values through the sandbox iframe like every other tier. You could equally render nothing at all and use it purely as a background computation engine (a headless test harness, a checksum verifier, an audio-rate signal generator wired to WebAudio).
Tier 4 is the right choice when:
- The schematic isn't what you want to show. A game frontend, a calculator UI, a hex memory editor, an animated state-machine diagram — anything where the canvas would be the wrong artifact.
- You want chrome that's meaningfully different from
CircuitEmbed's card and you'd rather render<CircuitCanvas>directly with no wrapping (it's also a public export — same composition, with the canvas). - You're rendering multiple circuits in a single composed panel where layout coordination matters.
- You're building a debugger / IDE-like surface that wants to expose sim actions to many sibling UIs.
For most pages, tier 2 is enough.
Two examples of tier 4 on simten.dev itself:
- Snake — the circuit is the game
engine (
DualPortRAMframebuffer, 4-phase pipeline, collision detection); the React component renders a custom 8×8 pixel grid + D-pad + speed slider on top of the sim. NoCircuitCanvasshown at all — the schematic is invisible to the player. Pure proof that the simulator can drive any frontend. - RV32I CPU debugger — a 5-stage pipelined RISC-V processor on the engine side, with a bespoke debugger UI on top: register file, memory view, pipeline-stage indicators, instruction stream. Compile C / Rust / asm in-browser and step through execution cycle by cycle.
Security
Composing the primitives directly doesn't bypass anything —
useCircuitSimulator routes every tier's simulation work through
the same cross-origin sandbox iframe that <CircuitEmbed /> uses
internally. See Security architecture for the
full picture (origin isolation, Web Worker thread isolation, CSP,
postMessage boundary).
Which tier do I pick?
| Tier | Pick when | API |
|---|---|---|
| 1 | You just want a circuit on a page | <CircuitEmbed circuit={X} /> |
| 2 | A sibling reacts to switch toggles | <CircuitEmbed onPortValuesChange={...} /> |
| 3 | A sibling drives the switches | ref={...} + setNodeValue |
| 4 | You want fully custom chrome / layout | useCircuitSimulator + CircuitCanvas |
Most production simten.dev pages use tier 1. The /learn/adders half-
and full-adder sections use tier 2 (live truth tables). Tier 3 isn't
currently used in the public site but the API is there. Tier 4 is the
escape hatch — keep it in mind for when you outgrow the canned embed.