Solid slots
This page shows Solid integration for plugin slots.
Use slots when you want external modules to contribute JSX.Element UI in host-defined regions without forking your app. The host keeps ownership of layout and slot typing; plugins only see the context and props you expose.
If you have not read the shared model yet, start with Plugin Slots.
Runtime-loaded external plugins
If your app loads plugins from disk at runtime (for example await import(fileUrl)), add this import once in your app entry:
import "@opentui/solid/runtime-plugin-support"
This is a Bun drop-in that installs runtime transform support so external TS/TSX plugin modules use the same runtime instances as the host app (@opentui/solid, @opentui/core, @opentui/core/3d, @opentui/core/testing, solid-js, and solid-js/store).
Use this for plugin systems in both normal Bun runs and standalone compiled executables.
import "@opentui/solid/runtime-plugin-support"
const mod = await import(pathToFileURL(pluginPath).href)
registry.register(mod.loadExternalPlugin())
What Solid adds
createSolidSlotRegistry(renderer, context, options?)— create a registry typed forJSX.Element. Accepts the sameSlotRegistryOptionsascreateSlotRegistry.Slot<TSlots, TContext>— generic slot component that takesregistryas a required propcreateSlot(registry, options?)— optional convenience helper that returns a registry-bound<Slot />componentSolidPlugin<TSlots, TContext>— convenience type alias for a plugin that returnsJSX.Element@opentui/solid/runtime-plugin-support— one-line runtime support for external plugin/module loading
Register plugins directly with registry.register() — no wrapper function is needed (unlike core’s registerCorePlugin).
Basic usage
import { createCliRenderer } from "@opentui/core"
import { createSolidSlotRegistry, Slot, render } from "@opentui/solid"
type Slots = {
statusbar: { user: string }
}
const context = { appName: "solid-app", version: "1.0.0" }
const renderer = await createCliRenderer()
const registry = createSolidSlotRegistry<Slots, typeof context>(renderer, context)
const unregister = registry.register({
id: "clock-plugin",
slots: {
statusbar(ctx, props) {
return <text>{`${ctx.appName}:${props.user}`}</text>
},
},
})
const AppSlot = Slot<Slots, typeof context>
const App = () => (
<AppSlot registry={registry} name="statusbar" user="sam" mode="replace">
<text>fallback-statusbar</text>
</AppSlot>
)
render(() => <App />, renderer)
Optional convenience helper
If you prefer not to pass registry each time, you can still bind one once:
const AppSlot = createSlot(registry)
<Slot> props
| Prop | Type | Required | Description |
|---|---|---|---|
registry | SlotRegistry<JSX.Element, Slots, Context> | yes | Registry to resolve plugins from |
name | keyof Slots | yes | Which slot to render |
mode | SlotMode | no | "append" (default), "replace", or "single_winner". See slot modes. |
pluginFailurePlaceholder | (failure: PluginErrorEvent) => JSX.Element | no | Per-slot placeholder UI when a plugin throws |
children | JSX.Element | no | Fallback UI |
| remaining | Slots[name] | — | Slot-specific props forwarded to plugin renderers |
SolidSlotOptions (for createSlot)
| Option | Type | Required | Description |
|---|---|---|---|
pluginFailurePlaceholder | (failure: PluginErrorEvent) => JSX.Element | no | Creates placeholder UI when a plugin throws |
Plugin failure placeholders
const Slot = createSlot(registry, {
pluginFailurePlaceholder(failure) {
return <text>{`plugin-error:${failure.pluginId}:${failure.phase}`}</text>
},
})
If a plugin throws, the slot renders the placeholder. If no placeholder is provided (or it returns null), the slot falls back to children.
Plugins that throw during the initial render call are caught inline. Plugins that throw during a Solid re-render are caught by an internal <ErrorBoundary> that reports the error to the registry.
Observability
registry.onPluginError((event) => {
console.error(event.pluginId, event.phase, event.source, event.error.message)
})
Example: packages/solid/examples/components/plugin-slots-demo.tsx