---
title: Slots catalog
sidebarTitle: Slots catalog
description: Every named slot the dashboard renders today. Each entry lists the host page, the slot name, and the context the slot's components receive.
---

This is the canonical list of slots the dashboard currently exposes. The source of truth is the `<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:

```tsx
import { usePermissions, useStore } from '@spree/dashboard'

function MyWidget({ product }: { product: Product }) {
  const { permissions } = usePermissions()
  const { store } = useStore()
  if (!permissions.can('read', 'MyApp::LoyaltyRecord')) return null
  // ...
}
```

The same applies to an entry's `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()`](recipes/custom-form-field.md) 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 |

### `collection.form_sidebar`

| Where | Collection detail page (`products/collections/$collectionId`), end of the sidebar column |
|---|---|
| Use | Collection-scoped cards — merchandising rules, feed settings, external sync state |
| Context | `{ collection }` — the `Collection` record |
| Host form | **Yes** — form key `collection` |

### `company.form_main`

| Where | Company detail page, end of the main column |
|---|---|
| Use | B2B account cards — credit terms, approval settings, external account references |
| Context | `{ company, kind, canEdit }` — `kind` is the company's own `kind`, `canEdit` whether the viewer may write |
| Host form | No — the widget owns its persistence |

### `company.form_sidebar`

| Where | Company detail page, end of the sidebar column |
|---|---|
| Use | Narrow company-scoped cards alongside the main form |
| Context | `{ company, kind, canEdit }` |
| Host form | No |

## Seller panel slots

Rendered by the marketplace seller panel (`@spree/seller-dashboard`) rather than the admin dashboard, and registered the same way.

### `seller.form_main`

| Where | Seller detail page, end of the main column |
|---|---|
| Use | Seller-scoped cards — payout configuration, onboarding state, compliance records |
| Context | `{ seller, canEdit }` |
| Host form | No |

### `seller.form_sidebar`

| Where | Seller detail page, end of the sidebar column |
|---|---|
| Use | Narrow seller-scoped cards |
| Context | `{ seller, canEdit }` |
| Host form | No |

### `seller.team.actions`

| Where | Seller team screen, in the header's action area |
|---|---|
| Use | Team-level actions — bulk invite, export, an external directory sync |
| Context | `{ sellerId }` |
| Host form | No |

### `seller.team.after`

| Where | Seller team screen, below the member list |
|---|---|
| Use | Anything that belongs under the team — audit log, role explainer, seat usage |
| Context | `{ sellerId }` |
| Host form | No |

## 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) |

Some pages override `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`

```ts
interface PaymentMethodEditorContext {
  mode: 'create' | 'edit'
  type: string                                    // STI shorthand (`stripe`, `bogus`, …)
  paymentMethod: PaymentMethod | null             // null in create mode
  preferenceSchema: PreferenceField[]
  preferences: Record<string, unknown>
  onPreferencesChange: (next: Record<string, unknown>) => void
  form: UseFormReturn<PaymentMethodFormValues>    // RHF instance for top-level fields
}
```

Helpers for building the slot name:

```ts
import {
  paymentMethodGuideSlot,
  paymentMethodFormSlot,
  paymentMethodActionsSlot,
} from '@spree/dashboard/components/spree/payment-method-editors/types'

paymentMethodFormSlot('stripe')  // → "payment_method.form.stripe"
```

Use these instead of constructing the string yourself, so a rename in one place doesn't silently break your registration.

## Adding a new slot

If a built-in page should expose a new injection point:

1. Pick a name (`<resource>.<area>`, e.g., `order.timeline`)
2. Add `<Slot name="..." context={{ resource: order, /* ... */ }} />` at the call site
3. Document the slot here — host, intent, context shape
4. Open the PR

The slot is not "live" until the docs land — without an entry here, no plugin author knows it exists.

## Reference

- [`<Slot>` source](https://github.com/spree/spree/blob/main/packages/dashboard-core/src/components/slot.tsx)
- [`slot-registry`](https://github.com/spree/spree/blob/main/packages/dashboard-core/src/lib/slot-registry.ts) — `registerSlot`, `removeSlot`, `updateSlot`, `useSlotEntries`
- [Slots customization page](customization/slots.md) — how to register against a slot
