Viewer tab plugins
View plugins add a custom visualization for one example snippet or one transform output. Tab plugins are a separate, parallel mechanism: they add a whole new top-level tab to a bblock’s detail page, driven by the full bblock (and its register) rather than a single piece of content — useful for anything that doesn’t fit “one visualization for one example,” such as a custom summary panel, a report generated from a build plugin’s output, or a view over the bblock’s metadata itself.
A tab plugin is a small client-side ES module the viewer loads at runtime (via a browser-native
import()), just like a view plugin, and decides for itself — given the whole bblock — whether it
applies. If it matches, the viewer adds an extra tab after all the built-in ones (About, Examples,
Data structure, JSON Schema, …), rendering the plugin’s own content full-page.
Adding a tab plugin to a register
Declare one or more plugins under viewer.tab-plugins in bblocks-config.yaml — the same shape as
view-plugins, just a sibling key:
viewer:
tab-plugins:
- url: https://example.org/my-plugin/dist/index.js
export: MyTabPlugin # optional; a single name, an array of names, or omitted for the default export
weight: 100 # optional; higher sorts earlier among tab plugins
url— the plugin’s ES module, fetched at runtime viaimport(). Must be served with permissive CORS (GitHub Pages does this by default).export— which export(s) of the module to use as plugin classes. Omit it (or set it tonull/""/[]) to use the module’s default export; set it to an array to pull in several plugin classes from the same file/bundle. A single bundle can mix view-plugin and tab-plugin classes, declared separately underview-plugins/tab-plugins.weight— controls ordering relative to other tab plugins (higher sorts earlier). Tab plugins always render after every built-in tab, regardless of weight.
This is written through to register.json (as viewer.tabPlugins, camelCased) and read by the
viewer at runtime — no postprocessing step touches the plugin code itself, so a register only ever
needs to point at where the plugin is hosted.
Tab plugins pair naturally with a build plugin that adds a new
field/document to a bblock’s json-full output — the build plugin emits the data, the tab plugin
renders it — but the two aren’t coupled at the mechanism level. A tab plugin’s matches() can key
off anything in the bblock or register, not just a specific field a build plugin added.
Writing a tab plugin
Start from the same bblocks-view-plugin-starter template repo used for view plugins — it includes two worked tab-plugin examples (a plain-JS “Used by” reverse-dependency tab, and a TS+Vue “Register info” tab) alongside the view-plugin ones. See that repo’s README, “Tab plugins” section, for the full walkthrough; the interface is summarized here.
Plugin interface
class MyTabPlugin {
// Route `section` slug and tab key — required for a stable link across rebuilds (recoverable
// if missing: the host synthesizes one from tabLabel, but declare it explicitly).
static tabId = 'my-tab';
// v-tab display text — required, no placeholder fallback if omitted (the plugin is skipped).
static tabLabel = 'My Tab';
// Icon — optional, falls back to a generic default. Either an MDI icon name (the default/
// simplest form, e.g. 'mdi-puzzle-outline'), or { url: '...' } pointing at a square PNG/JPG/SVG
// image for a custom (non-MDI) icon. Use an absolute URL — a relative one resolves against the
// viewer's own page origin, not the plugin's.
static icon = 'mdi-puzzle-outline';
// static icon = { url: 'https://example.org/icons/my-tab.svg' };
// Ordering among other matched tab plugins for the same bblock — optional, default 0.
static weight = 0;
// Whether the instance + rendered DOM persist across same-bblock tab switches (default true) or
// are torn down/rebuilt every time the tab is left/reactivated (false) — set false only for
// something holding a live connection/poller that shouldn't run in the background.
static cacheable = true;
// context: the full bblock and register objects, plus a small document-fetching facade — see
// below. Unlike a view plugin, there's no per-candidate constructor argument: a tab plugin's
// decision is over the whole bblock, not one example/transform-output representation of it.
constructor(context) {
this.context = context;
}
// Whole-context predicate deciding whether this plugin contributes a tab for this bblock — not
// bound to checking a single field. May be async. Default true if unimplemented.
async matches() { return true; }
// el: an empty container for the *entire* tab body (not a small chrome-wrapped box like a view
// plugin gets) — no fullscreen-toggle chrome is provided. May be async.
render(el) { /* mount here */ }
// Optional teardown — called when the tab/bblock unmounts (see `cacheable` above).
destroy(el) {}
}
The context object passed to the constructor:
interface TabPluginContext {
bblock: object; // the full, already-fetched bblock (json-full shape)
register: object | null; // the full, already-constructed register object
viewerConfig: object | null; // the viewer's resolved runtime config
depResolver?: DependencyResolver; // optional shared-dependency cache, same as view plugins
fetchDocument(bblock, property): Promise<unknown>;
fetchDocumentByUrl(bblock, url, options?): Promise<unknown>;
getBBlock(itemIdentifier): Promise<object | undefined>;
getBBlocks(includeRemote?): Promise<Record<string, object>>;
}
fetchDocument/fetchDocumentByUrl/getBBlock/getBBlocks are the same helpers regular viewer
components use to fetch a bblock’s own documents or look up other bblocks — handy if a tab plugin
wants to check or render something beyond what’s already inline in context.bblock.
Bundle any third-party dependency (including a UI framework, if you want one — render(el) is a
plain DOM-element handoff, so createApp(...).mount(el)/React’s createRoot(el).render(...) work
the same way a view plugin could mount Three.js into its box) into your own plugin module rather
than assuming the host provides it. See View plugins
and the starter template’s README for the shared guidance (lazy-loading heavy dependencies,
context.depResolver, CSS injection) that applies identically here.
Trying it out
Same as for view plugins: build the plugin, serve dist/ from any static host with permissive
CORS, and point bblocks-config.yaml at it as shown above. See
View plugins → Trying it out for the full walkthrough
(jsDelivr hosting via the starter template, local testing tips, and why the devtools console
matters for anything an async error in your own plugin code wouldn’t otherwise surface).
See a tab plugin working live on the
tab-plugin-demo bblock
in the bblocks-examples register.
Trust model
Same as view plugins: a register only ever loads the tab plugins it declares itself — plugins from
imported registers are never consulted. Declaring a plugin’s URL is equivalent to embedding a
<script> on a page you control: no sandboxing is applied, so only point at code you trust.