<Slot name="…"> call sites in the dashboard source (linked under Reference below); each entry here records the host page, the slot name, and the context your component receives.
If you need a new injection point in a built-in page, open a PR adding <Slot name="..." context={...} /> and a documentation entry here — that’s the contract for new slots.
Ambient context
Ambient context (permissions, store, user merged into every slot’s props) is planned but not wired up yet — today slot components receive only the slot-specific context listed below. Until it lands, read those values with the hooks instead:
if predicate: it receives the slot context, but not permissions — gate inside the component for now.
Page header slots
Rendered by<PageHeader> (@spree/dashboard-core/components/page-header.tsx) at the top of every detail and list page that uses the shared chrome.
page.actions
| Where | Top-right of <PageHeader>, left of any explicit actions prop |
|---|---|
| Use | Add primary action buttons (e.g., a “Refund” button on the order detail page) |
| Context | { resource, ...slotContext } — resource is the current page’s primary record, when the host passed one to <PageHeader resource={...} />. May be undefined on list pages. |
page.actions_dropdown
| Where | Inside the more-actions (⋯) dropdown menu, above the auto-rendered Copy ID / Delete items |
|---|---|
| Use | Add menu items (<DropdownMenuItem>) for secondary actions |
| Context | { resource, ...slotContext } — same as page.actions |
Detail-page form slots
The resource detail pages each render a slot below their built-in cards. The context key matches the resource name. Host form: on pages marked host form: yes, the slot renders inside the page’s own<form> and the form context is exposed — widgets can bind inputs via useHostForm() that hydrate, dirty-track, and save with the page’s Save button. On pages without one, widgets own their persistence (use useOptionalHostForm() to adapt).
product.form_sidebar
| Where | Product detail page (products/$productId), end of the sidebar column |
|---|---|
| Use | Add a card scoped to the product being edited (e.g., a “Brand” picker, wishlist stats, sync status from an external system) |
| Context | { product } — the full Product record from the Admin API |
| Host form | Yes — form key product |
category.form_sidebar
| Where | Category detail page (products/categories/$categoryId), end of the sidebar column |
|---|---|
| Use | Category-scoped cards — merchandising rules, feed settings, external sync state |
| Context | { category } — the Category record (may be briefly undefined while refetching) |
| Host form | Yes — form key category |
store.form_main
| Where | Store settings page (settings/store), end of the main column |
|---|---|
| Use | Store-level settings a plugin owns — integration toggles, account linking |
| Context | { store } — the full Store record |
| Host form | Yes — form key store |
order.form_sidebar
| Where | Order detail page (orders/$orderId), end of the sidebar column |
|---|---|
| Use | Fraud-check status, shipping-integration state, loyalty points earned |
| Context | { order } — the full Order record |
| Host form | No — widgets save via their own API calls |
customer.form_sidebar
| Where | Customer detail page (customers/$customerId), end of the sidebar column |
|---|---|
| Use | Loyalty status, support-ticket summary, external CRM links |
| Context | { customer } — the full Customer record |
| Host form | No — the page edits through sheets; widgets save via their own API calls |
Page tabs slot
page.tabs (default)
Rendered by <PageTabs> at the right edge of any tabbed sub-nav.
| Where | After the built-in tab strip |
|---|---|
| Use | Append your own tab(s) to a sub-route — typically for plugin-owned views inside an existing resource (e.g., “Returns” tab on the order detail) |
| Context | { tabs, ...slotContext } — tabs is the array of built-in tabs (so you can inspect or filter by them) |
slotName to scope tabs to a single resource (e.g., slotName="order.tabs"). The catalog will grow these as we wire them up; today only the default page.tabs name is in production use.
Payment method editor slots (dynamic)
Used by<PaymentMethodForm> to let payment-provider plugins replace pieces of the editor sheet. The slot names are computed per provider type (stripe, bogus, …), so registering against the right name is what hooks your editor in.
payment_method.guide.<providerType>
| Where | Above the preferences form |
|---|---|
| Use | Banner explaining what the integration does, what credentials to use, links to provider docs |
| Context | PaymentMethodEditorContext (see below) |
payment_method.form.<providerType>
| Where | In place of the auto-generated preferences form |
|---|---|
| Use | Render a custom React form when the provider’s preferences are too complex for the generated UI (multi-step OAuth, environment switchers, …) |
| Context | PaymentMethodEditorContext |
payment_method.actions.<providerType>
| Where | Sheet footer, before Save/Cancel |
|---|---|
| Use | Provider-specific action buttons — “Test connection”, “Rotate keys”, “Open dashboard in provider” |
| Context | PaymentMethodEditorContext |
PaymentMethodEditorContext
Adding a new slot
If a built-in page should expose a new injection point:- Pick a name (
<resource>.<area>, e.g.,order.timeline) - Add
<Slot name="..." context={{ resource: order, /* ... */ }} />at the call site - Document the slot here — host, intent, context shape
- Open the PR
Reference
<Slot>sourceslot-registry—registerSlot,removeSlot,updateSlot,useSlotEntries- Slots customization page — how to register against a slot

