Extension slots
Extension slots let one pilet declare a named placeholder in its UI that any other pilet can fill — without either pilet knowing anything about the other. The app shell's extension registry connects them at runtime.
Declaring a slot
In any component, render <ExtensionSlot> from piral:
If no pilet has registered for this name, nothing renders. You can provide fallback content for the empty state:
Filling a slot
From any pilet's setup function:
Multiple pilets can register for the same slot — all contributions render in feed-response order.
Passing parameters
The slot can pass contextual data to all registered components:
Extension components receive params as a prop:
Conditionally wrapping a slot
There's no useExtensions hook — the ExtensionSlot decides what to render. To wrap a slot in a section only when something is registered, use its render prop. It receives the already-rendered extensions and lets you return your own markup around them (or nothing at all):
This works anywhere a slot does, including inside a pilet — it's just props on the component, not a shell-only hook.
For the simpler "show a fallback when the slot is empty" case, pass empty (and emptySkipsRender to render nothing instead) rather than checking yourself — see Declaring a slot.
If you genuinely need the raw registrations, a shell component can read them from global state: useGlobalState((s) => s.registry.extensions['cart-extra'] ?? []). This is shell-only (pilets can't use it) and couples you to internal state shape — prefer the render prop above.
Limiting extension count
Extension slots in the app shell layout
Well-designed app shells use extension slots in their layout components. This lets pilets contribute navigation items, header actions, and sidebar widgets without the shell explicitly listing any pilet:
Slot naming conventions
Slot names are the coordination surface between pilets. They are effectively public API — treat them as such.
- Kebab-case:
cart-extra, notCartExtra - Namespace with context:
checkout-cart-extrato avoid conflicts with other pilets' slots - Describe purpose, not location:
product-quick-actionsrather thanproduct-page-right-col - Document your slots: pilet developers need to know what slots exist, what
paramsshape they receive, and how many extensions render
Changing a slot name or its params shape is a breaking change for every pilet that fills it. Version accordingly.
Unregistering
You rarely need to do this manually. Everything a pilet registers is automatically torn down when the pilet is hot-reloaded during development, updated to a new version, or removed from the feed — so you don't have to clean up on the pilet's behalf.
Calling unregisterExtension yourself only makes sense in genuinely dynamic scenarios: an extension that should be available for a short time window, or only while some complex runtime condition holds. To unregister, keep a reference to the exact same component you registered: