Toolbench

Host API

defineToolHost, the element's attributes, properties, methods and events, and tool sources.

A host is the page that shows tools. It defines the <tool-host> element once, with a source of tools, and then writes <tool-host> wherever a tool belongs.

TypeScript
defineToolHost({  source: new RegistrySource({ ...registry, ...extra }),  workerFactory: () => new Worker(new URL("./tool.worker.ts", import.meta.url), { type: "module" }),  // Relative, so the same pages work at / locally and under a sub-path on Pages.  pageUrl: (id) => `./gallery.html#tool-${encodeURIComponent(id)}`,  styles: all,  ...(highlight ? { highlight } : {}),});

How this site does it, from site/src/host.ts.

defineToolHost

defineToolHost(options, tagName = "tool-host"). Defines the element. Call once, with the source your host provides.

Options

AttributeTypeRequiredDefaultNotes
sourceToolSourceyesWhere tools come from.
workerFactory() => WorkernoRequired only for tools that declare thread: "worker". See the README.
pageUrl(id: string) => stringnoGiven a tool id, the URL of its full page, which card and embed modes link out to.
rootMarginstringnoHow far ahead of the viewport page and embed modes start loading. Default "200% 0px".
debounceMsnumbernoMilliseconds of quiet before a typed change runs. Default 150.
highlight(source: string, lang: string) => NodenoPaint a code result with the host's own highlighter. The runtime does not bundle one: that would roughly double the package, and a host usually already has one. Return a Node, not a string. A string-returning hook would need innerHTML, which SECURITY.md forbids. Omit the hook to keep readable preformatted text.
stylesHostStylesnoThe host's own stylesheets, adopted into every tool's shadow root after the runtime's. A sheet, CSS text, a list of either, or { "*": [...], "tool-id": [...] } for one tool to differ. The class names and attributes they may rely on are listed in STYLING.md.
wide"fit" | "scroll"noA graph wider than its plot: fit (default) scales it down to the plot's width, scroll draws it at its natural size in a plot that scrolls. A tool's wide attribute overrides it.
wideTogglebooleannoWhether a scaled graph offers the reader a button to switch between the two. Default true.
share"none" | ShareSchemenothe query-parameter schemeThe share button on page and embed tools, a link icon that copies a link to the tool with its inputs. By default the link is this page's URL with each input as a query parameter named <tool id>.<input id>, ?percentiles.method=linear, and a tool opening on a page reads its values back from those. "none" removes the button and reads nothing; a { url, read } pair is your own scheme. Inputs marked share: false are never written or read. A tool's share attribute overrides it.
shareLabelstringnoText beside the share button's icon, which is otherwise shown alone. A tool's share-label overrides it.
sharePosition"top" | "actions"no"top" on a page, "actions" in an embedWhere the share button sits: top, the end of the title row, or actions, the end of the Run row. A tool's share-position overrides it.

setHostStyles

setHostStyles(styles). Replace the host's stylesheets on every tool, on the page now and created later, without reloading: a theme switch, say. undefined removes them. Takes what styles takes.

Element attributes

What <tool-host> reads from its markup. Changing one redraws the tool and keeps the reader's inputs and result.

AttributeTypeRequiredDefaultNotes
toolstringyesThe id of the tool to show, as its manifest declares.
mode"card" | "page" | "embed"no"page"card: name, blurb, the primary inputs and the first result part, fetching the tool's code only when the reader opens it. page: every input, the full result and the tool's links. embed: page without the title, for the middle of an article. page and embed fetch when they scroll near the viewport.
partsnumberno1How many parts of a grouped result to show. A card always caps; on page and embed there is no cap unless this is set.
more"link" | "expand"no"link"What stands for the parts parts hid: link, a line of text pointing at the full tool, or expand, a button that shows them in place without running the tool again.
layout"default" | "answer-first"no"default"answer-first draws the result above the form, for a tool that opens a page about something else.
controls"default" | "none"no"default"none draws the result and nothing else: no form, no Run, no status line. The page drives the tool through values and run().
wide"fit" | "scroll"no"fit"A graph wider than its plot: fit scales it down to the width, scroll draws it full size in a plot that scrolls. Overrides defineToolHost({ wide }).
wide-toggle"true" | "false"no"true"false hides the expand and fit button on a scaled graph. Overrides defineToolHost({ wideToggle }).
reset"default" | "none"no"default"none leaves out the Reset button, for a tool where it adds nothing, such as one with a single input.
links"all" | "page" | "none"no"all"What goes under the tool: all, the manifest's links; page, only "Open the full tool", pointing at pageUrl(id); none, nothing. For a tool embedded in an article, where a row of references sits between the answer and the prose. A card shows "Open the full tool" for anything but none. A retired tool keeps its links whatever this says, because its notice points at them. Also a property, host.links.
share"default" | "none"no"default"none leaves out the share button, and the tool then reads nothing from the page's URL either. Overrides defineToolHost({ share }).
share-labelstringno""Text beside the share button's icon, which is otherwise shown alone. Overrides defineToolHost({ shareLabel }).
share-position"top" | "actions"no"top" on a page, "actions" in an embedWhere the share button sits: top, the end of the title row, or actions, the end of the Run row. Overrides defineToolHost({ sharePosition }).

Example

HTML
<tool-host tool="percentiles" mode="embed" parts="1" more="expand" data-run></tool-host>

A gallery card: one part shown, the rest behind a button.

See it live: Part of a result, and the rest on request

Properties and methods

On the element, for a page that drives a tool itself.

MemberUseWhat it does
values (set)values = { ... }Prefill the form. Partial is fine: unnamed inputs keep their current value.
values (get)valuesA copy of the current input values, including ones a compact card has not rendered.
run()run({ focus? }): Promise<void>Run the tool. Opt-in: setting values never does this on its own.
reveal()reveal(id): Promise<boolean>Bring one part of the result to the reader: the part whose id is id. Resolves true once it is on screen with focus on it, false if the result has no such part.
seedseed = outputA precomputed result to show before anything runs.

Example

TypeScript
// Fill the form the way typing would. It does not run the tool.if (host.dataset.values) host.values = JSON.parse(host.dataset.values) as Record<string, unknown>;// Press Run once, on the reader's behalf, so the example opens with its answer.if (host.hasAttribute("data-run")) void host.run();
TypeScript
const host = document.querySelector<Host>("#reveal-host");for (const button of document.querySelectorAll<HTMLButtonElement>("[data-reveal]")) {  // Opens the part if `parts` hid it, scrolls to it and moves focus there.  button.addEventListener("click", () => void host?.reveal(button.dataset.reveal ?? ""));}

From site/src/gallery.ts.

Events

Dispatched on <tool-host>. Each bubbles and is composed, so a listener on the document hears every tool. None fires while the element starts up with its first values.

EventdetailWhen
tool-input{ values: InputValues }The inputs changed, by typing, a sample, a preset or a write to values. Once per change, after the form shows the new values, and only when they differ from the last ones reported. Dispatched before a values write returns. values is a copy, already clamped and refused the way typing is. Debounce is the listener's business.
tool-run{ values: InputValues; ok: boolean; error?: string }A run settled: from Run, Enter, run(), reveal() or an autoRun tool's typing. Once per run, after its result or error is drawn and before run() resolves. values are the ones the run used. ok is false for an error result and for a run that failed, with error the message the reader sees. A run replaced by a newer one before it finished dispatches nothing.
tool-reset{ values: InputValues }The reader pressed Reset. Dispatched after the form shows the starting values and the result is cleared, and after the tool-input for the same change. values are the starting values.
tool-share{ url: string; values: InputValues }The reader pressed the share button. After the copy is attempted, whether or not the clipboard took it. url is the link, values the inputs it carries: every input but one marked share: false.

Example

TypeScript
const log = document.querySelector("#events-log");for (const type of ["tool-input", "tool-run"]) {  // Both bubble out of the shadow root, so the listener could as well be on `document`.  document.querySelector("#events-host")?.addEventListener(type, (event) => {    const { detail } = event as CustomEvent<Record<string, unknown>>;    if (log) log.textContent = `${type} ${JSON.stringify(detail, null, 1).replace(/\s*\n\s*/g, " ")}`;  });}

From site/src/gallery.ts.

See it live: Following the tool from your page

Sources

Where tools come from. RegistrySource is the one most hosts use; a host may write its own ToolSource.

ToolSource

AttributeTypeRequiredNotes
list()Promise<Manifest[]>yesEvery tool this source can offer, already validated and brought up to the current contract.
load()Promise<LoadedTool>yesThe manifest plus the module, ready to run.

RegistrySource

new RegistrySource(entries). The everyday source: a map of ids to entries, which a host builds however it likes: by hand, or from a bundler's directory glob.

RegistryEntry

AttributeTypeRequiredNotes
manifestunknownyesThe parsed tool.json, unchecked: RegistrySource validates and upgrades it.
load()Promise<ToolModule>yesA lazy import of the tool's module, called only when the tool is opened or scrolled near.
TypeScript
export function entries(m: Record<string, unknown>, code: Record<string, () => Promise<unknown>>): Record<string, RegistryEntry> {  return Object.fromEntries(    Object.entries(m).map(([path, manifest]) => {      const dir = path.slice(0, path.lastIndexOf("/"));      const id = dir.split("/").at(-1) ?? path;      const load = code[`${dir}/index.ts`];      if (!load) throw new Error(`${id} has a tool.json but no index.ts beside it`);      return [id, { manifest, load: load as RegistryEntry["load"] }];    }),  );} export const registry = entries(manifests, modules);

How this site builds its registry from a directory glob, from site/src/registry.ts.