Core slots
This page shows the core host API for plugin slots.
Use slots when you want external modules to render BaseRenderable UI in host-defined layout regions without forking the app. The host keeps control of layout and slot typing; plugins only render through the APIs you expose.
If you have not read the shared model yet, start with Plugin Slots.
What core adds
On top of createSlotRegistry, core provides:
createCoreSlotRegistry— create a registry typed forBaseRenderablenodesregisterCorePlugin— register a plugin using the core-specificCorePlugininterfaceSlotRenderable— aRenderablethat mounts a slot into the renderable treeresolveCoreSlot— resolve slot entries without mounting@opentui/core/runtime-plugin-support/createRuntimePlugin— runtime module support for external plugin/module loading in Bun
Core slot renderers receive both the host context and slot data: (ctx, data) => BaseRenderable.
createCoreSlotRegistry accepts the same SlotRegistryOptions as createSlotRegistry.
Runtime-loaded external plugins
If your app loads plugin modules from disk at runtime, import this once in your app entry:
import "@opentui/core/runtime-plugin-support"
This installs Bun runtime support for @opentui/core plus default core runtime entrypoints (@opentui/core/3d, @opentui/core/testing).
If you need additional host-resolved modules, register your own plugin:
import { plugin } from "bun"
import { createRuntimePlugin } from "@opentui/core/runtime-plugin"
plugin(
createRuntimePlugin({
additional: {
"my-runtime-module": async () => (await import("./runtime-module")) as Record<string, unknown>,
},
}),
)
CorePlugin interface
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique identifier. Duplicate ids throw. |
order | number | no | Sort priority (ascending). Defaults to 0. |
setup | (ctx, renderer) => void | no | Called once at registration time. If it throws, the plugin is not registered. |
dispose | () => void | no | Called when the plugin is unregistered or the registry is cleared. |
slots | Partial<Record<SlotName, CoreSlotContribution>> | yes | Each value is a renderer function or a managed slot object. |
A CoreSlotContribution is either a plain renderer (ctx, data) => BaseRenderable or a CoreManagedSlot with lifecycle hooks.
Basic usage
A SlotRenderable extends Renderable, so it can be added to any parent like a BoxRenderable. It resolves plugins from the registry, manages their lifecycle, and reconciles their output as its children.
import {
BoxRenderable,
createCliRenderer,
createCoreSlotRegistry,
registerCorePlugin,
SlotRenderable,
TextRenderable,
} from "@opentui/core"
type Slots = "statusbar"
type SlotData = { label: string }
const context = { appName: "core-app", version: "1.0.0" }
const renderer = await createCliRenderer()
const registry = createCoreSlotRegistry<Slots, typeof context, SlotData>(renderer, context)
const unregister = registerCorePlugin(registry, {
id: "clock-plugin",
order: 0,
slots: {
statusbar(_ctx, data) {
return new TextRenderable(renderer, {
id: "clock-status",
content: `clock: ${data.label}`,
})
},
},
})
const slot = new SlotRenderable(renderer, {
id: "statusbar-slot",
registry,
name: "statusbar",
data: { label: "ok" },
mode: "append",
width: "100%",
height: 3,
flexDirection: "row",
fallback: () =>
new TextRenderable(renderer, {
id: "statusbar-fallback",
content: "fallback",
}),
})
renderer.root.add(slot)
// later
slot.mode = "replace"
slot.data = { label: "updated" }
slot.refresh()
slot.destroy()
registerCorePlugin returns an unregister function (() => void). Calling it removes the plugin and invokes its dispose hook.
Because SlotRenderable extends Renderable, it accepts all standard layout options (width, height, flexDirection, padding, etc.) alongside the slot-specific options.
SlotRenderable options
| Option | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique renderable identifier (inherited from RenderableOptions) |
registry | CoreSlotRegistry | yes | The registry to read plugins from |
name | slot name | yes | Which slot to mount |
data | object | no | Slot data passed to plugin renderers as the second argument |
mode | SlotMode | no | "append" (default), "replace", or "single_winner". See slot modes. |
fallback | BaseRenderable | BaseRenderable[] | () => ... | no | Fallback nodes or a factory that creates them |
pluginFailurePlaceholder | (failure, ctx) => BaseRenderable | BaseRenderable[] | undefined | no | Creates placeholder UI when a plugin throws |
| …layout options | RenderableOptions | no | Standard layout props: width, height, flexDirection, padding, etc. |
Instance API
| Member | Description |
|---|---|
mode | Getter/setter. Changing the mode automatically refreshes the slot. |
refresh() | Re-resolve plugins and reconcile mounted children. |
destroy() | Inherits from Renderable. In addition to the standard cleanup, SlotRenderable unsubscribes from the registry, calls onDeactivate on active managed slots, then calls onDispose on all managed slots. Host-owned nodes (plain function contributions) are destroyed; plugin-owned nodes (managed slot contributions) are detached but left for the plugin to clean up in onDispose. |
resolveCoreSlot
Resolve slot entries without mounting them. Useful when you need to inspect or render plugin output manually.
import { resolveCoreSlot } from "@opentui/core"
const entries = resolveCoreSlot(registry, "statusbar")
// Array<{ id: string, renderer: (ctx, data) => BaseRenderable }>
Plugin failure placeholders
If a plugin throws during slot render, you can provide host-controlled fallback UI for that plugin failure:
const slot = new SlotRenderable(renderer, {
registry,
name: "statusbar",
fallback: () => new TextRenderable(renderer, { id: "fallback", content: "fallback" }),
pluginFailurePlaceholder(failure, ctx) {
return new TextRenderable(renderer, {
id: `error-${failure.pluginId}`,
content: `plugin error: ${failure.pluginId}`,
})
},
})
Managed slot contributions
A core slot contribution can be a plain function or a managed slot object with lifecycle hooks:
registerCorePlugin(registry, {
id: "managed-plugin",
slots: {
statusbar: {
render(_ctx, data) {
return new TextRenderable(renderer, { id: "managed", content: "managed" })
},
onActivate(ctx) {
// plugin became active in this slot (visible)
},
onDeactivate(ctx) {
// plugin is no longer active (e.g. mode changed to single_winner and this plugin lost)
},
onDispose(ctx) {
// plugin is removed/disposed for this slot
},
},
},
})
CoreManagedSlot interface
| Field | Type | Required | Description |
|---|---|---|---|
render | (ctx, data) => BaseRenderable | yes | Creates the renderable node for this slot |
onActivate | (ctx) => void | no | Called when the plugin becomes active (visible) in the slot |
onDeactivate | (ctx) => void | no | Called when the plugin is no longer active (e.g. lost single_winner) |
onDispose | (ctx) => void | no | Called when the plugin is removed or the slot is destroyed |
Node ownership
When a slot contribution is a plain function, the host owns the returned nodes. On deactivation or disposal, the host detaches and destroys them.
When a slot contribution is a managed slot object, the plugin owns the nodes. On deactivation the host detaches them but does not destroy them — the plugin can reuse them if it becomes active again. On disposal, onDispose is called so the plugin can clean up.
Observability
registry.onPluginError((event) => {
console.error(event.pluginId, event.phase, event.source, event.error.message)
})
Example: packages/core/src/examples/core-plugin-slots-demo.ts