create-slot
Features declare what they render. Layouts decide where it appears.
A feature contributes UI to a named slot. A layout mounts a host for that slot and gets every contribution, in order, with its own props. Neither side imports the other, and nothing is drilled through the tree to connect them.
npm install create-slotSee it work
Toggle a feature. It is mounted somewhere else entirely — the sidebar never imports it, and it never imports the sidebar. The host renders its own children until something is contributed, and every contributed item reads the host's live props.
import { } from "create-slot"
export const = {
: <{ : string }>(),
}
export function ({ }: { : string }) {
return (
<>
<>
<>Home</>
<.. ={}>
<>Nothing installed yet</>
</..>
</>
</>
)
}
export function () {
return (
<. ={10}>
< />
</.>
)
}
function () {
const { } = ..()
return (
< ={ === "/pricing" ? "page" : }>Pricing</>
)
}Two channels
A contribution can be registered in two ways, and the difference is what a contribution is.
| Declarative | Runtime | |
|---|---|---|
| A contribution is | data plus a component, under an id | an element, from where it is mounted |
| Registered | before render, by resolvePlugins | from an effect, while mounted |
| In server HTML | yes | no |
| Addressable | disable, override, diagnostics — by full id | no identity |
| Shared state inside one feature | a store | ordinary useState |
| Per-contribution isolation | identity + error boundary + Suspense | Suspense with a null fallback |
| API | defineSlot, definePlugin, resolvePlugins, SlotProvider, SlotHost | createSlot |
Each channel keeps its own hosts — the registry never merges fills — and a single application uses both: the registry for anything that must be in the HTML, the façade for chrome that depends on live tree state. Two channels is the whole comparison, with the decision written out.
The declarative half of the same idea — a named slot, a plugin that declares a
contribution under an id, one resolvePlugins call, and a provider the
application mounts once:
// slots.ts — pure data, importable from server modules too
import { } from "create-slot/core"
export const = <{ : string }>("nav-menu")
export const = ("status-bar")
// plugins/pricing.tsx
import { } from "create-slot/core"
export const = ({
: "pricing",
: [
// "nav-item" is the contribution's id: required, unique inside the plugin.
// The full id is "pricing/nav-item" — the React key and override target.
.("nav-item", { : 10, : }),
],
})
function ({ }: { : string }) {
// An ordinary component: hooks, context, data fetching, all of it.
if ( === "/checkout") {
return null
}
return <>Pricing</>
}
// app.tsx
import { , , } from "create-slot"
// One pure function turns the plugin list into a Resolution. Resolve at
// module scope or in a `useMemo` — the provider never rebuilds anything.
const = ([])
export function ({ }: { : string }) {
return (
< ={}>
<>
< ={} ={{ : }}>
<>No plugins installed</>
</>
</>
</>
)
}The Resolution is plain data, and resolvePlugins is React-free — a server
component can resolve the graph and
hand it across the RSC boundary whole.
What it does not do
The library knows who contributes what, in what order, and what happens when a contribution breaks. Everything else is yours:
- No
whenpredicate. A contribution returnsnull. - No exclusive slots and no routing. "Exactly one owner" is a table your application resolves before render, where it can report the plugin it refused.
- No inventory helper. The manifest is data, so an inventory is a
.map. - No state, no lifecycle, no command registry. The manifest is open data you extend with your own fields, and they keep their types.
Zero dependencies, two entry points — one of them React-free — and any React
with useSyncExternalStore: 18 and up.
Where to go next
- Get started — a first contribution in three steps.
- Slots, hosts & fills — the four words this library uses, and what each one does.
- Plugin registry — the declarative channel end to end, and what you need for server rendering.
- API — every export, with the types twoslash infers from the published package.