Get started
Contribute your first piece of UI in three steps.
Pick the channel first
create-slot delivers a contribution to a host through two channels, and the
choice is the first thing to make, not the last:
| Declarative | Runtime | |
|---|---|---|
| API | defineSlot, definePlugin, resolvePlugins, SlotProvider, SlotHost | createSlot |
| In server-rendered HTML | yes | no |
| Needs a provider | yes | no |
| Steps on this page | the last section | the next three |
This page teaches the runtime channel, because it is the shortest path and needs no provider. Two channels is the full comparison.
Install
npm install create-slotThe package has no dependencies and two entry points: create-slot, and the
React-free create-slot/core for server modules. React is a peer dependency;
anything with useSyncExternalStore works, so React 18 and up.
1. Define the slot
A slot is a name plus a props type. The props are the host's — whatever the place that renders contributions knows and the contributions might want.
import { } from "create-slot"
export const = {
: <{ : string }>(),
}Define slots in their own module. It is the only thing both sides import, which is what keeps the feature and the layout from importing each other.
2. Mount a host
The host is where contributions render. Its own children are the placeholder: they render only while nothing is contributed, so an empty state costs no extra API.
export function ({ }: { : string }) {
return (
<>
<>
<>Home</>
<.. ={}>
<>Nothing installed yet</>
</..>
</>
</>
)
}You can mount as many hosts for one slot as you like — one per row of a table, for instance. Each renders the same contributions, with its own props.
3. Contribute from anywhere
The slot component itself is the contributor. It renders nothing where it sits; it registers its child into the slot for as long as it is mounted.
export function () {
return (
<. ={10}>
< />
</.>
)
}order is a priority, not an array index. Leave gaps — 10, 20, 30 — so a
feature can be inserted later without renumbering anything. See
Ordering.
Read the host's props
The contributed element is written next to the feature but rendered where the host is, so it reads the host's props through a hook rather than receiving them:
function () {
const { } = ..()
return (
< ={ === "/pricing" ? "page" : }>Pricing</>
)
}That is what makes one contribution adapt to many hosts: useProps reflects the
host doing the rendering, not the place the element was written.
Install the feature
Installing a feature is mounting it. There is no registration call, no
provider, and no array to keep in sync — switching a feature off is a && that
stops rendering it.
export function () {
return (
<>
< ="/pricing" />
< />
</>
)
}The same feature, declared
The declarative channel is the same three ideas — a slot, a host, a contribution — with one difference: the contribution is data plus a component, so the resolver enumerates it before render and the server can put it in the HTML.
Five changes to the code above:
- The slot gains a name:
defineSlot<Props>("nav-menu"). - The fill becomes a contribution with a required id:
NavMenu.contribute("nav-item", { order, component }). - Contributions are grouped into a plugin with a unique
id. - The application calls
resolvePluginsonce, turning the plugin list into aResolution. - The application mounts
SlotProviderwith that Resolution, and the host becomesSlotHost.
// 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</>
}The contribution receives the host's props as its own, so there is no
useProps in this version. Resolve once, and mount the provider above every
host:
// 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 two channels keep their own hosts — the registry never merges fills — but one application uses both freely. See Two channels.
Next
- Slots, hosts & fills — placeholders, multiple hosts,
useProps - Two channels — the full comparison, with the decision written out
- Plugin registry — the declarative channel end to end
- API — every export, with its inferred types
- Errors — every message the library throws, and its fix