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 for JSX.Element. Accepts the same SlotRegistryOptions as createSlotRegistry.
  • Slot<TSlots, TContext> — generic slot component that takes registry as a required prop
  • createSlot(registry, options?) — optional convenience helper that returns a registry-bound <Slot /> component
  • SolidPlugin<TSlots, TContext> — convenience type alias for a plugin that returns JSX.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

PropTypeRequiredDescription
registrySlotRegistry<JSX.Element, Slots, Context>yesRegistry to resolve plugins from
namekeyof SlotsyesWhich slot to render
modeSlotModeno"append" (default), "replace", or "single_winner". See slot modes.
pluginFailurePlaceholder(failure: PluginErrorEvent) => JSX.ElementnoPer-slot placeholder UI when a plugin throws
childrenJSX.ElementnoFallback UI
remainingSlots[name]Slot-specific props forwarded to plugin renderers

SolidSlotOptions (for createSlot)

OptionTypeRequiredDescription
pluginFailurePlaceholder(failure: PluginErrorEvent) => JSX.ElementnoCreates 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