306 lines
9 KiB
Markdown
306 lines
9 KiB
Markdown
---
|
|
tags: [payloadcms, tech-patterns]
|
|
topic: payloadcms
|
|
sources: [raw/custom-components__overview.md, raw/custom-components__root-components.md, raw/custom-components__dashboard.md, raw/custom-components__edit-view.md, raw/custom-components__list-view.md, raw/custom-components__document-views.md, raw/custom-components__custom-views.md, raw/custom-components__custom-providers.md]
|
|
created: 2026-05-15
|
|
---
|
|
|
|
# PayloadCMS — Custom Components
|
|
|
|
## Overview
|
|
|
|
- Every part of the admin UI can be replaced or augmented with your own React components
|
|
- All custom components are **RSC by default**; add `'use client'` to opt into client mode
|
|
- Payload auto-strips non-serializable props before passing them to client components
|
|
- Components are referenced by **file path strings** (not imports) to keep the Node config lightweight
|
|
- An `importMap.js` is auto-generated at startup — never edit it directly
|
|
|
|
## Config / Setup
|
|
|
|
### Component path syntax
|
|
|
|
```ts
|
|
// Named export via hash
|
|
Button: '/src/components/Logout#MyComponent'
|
|
|
|
// Default export
|
|
Button: '/src/components/Logout'
|
|
|
|
// Object form with extras
|
|
Button: {
|
|
path: '/src/components/Logout',
|
|
exportName: 'MyComponent',
|
|
clientProps: { label: 'Sign out' },
|
|
serverProps: { someServerData: true },
|
|
}
|
|
```
|
|
|
|
Set `admin.importMap.baseDir` to shorten paths:
|
|
|
|
```ts
|
|
admin: {
|
|
importMap: { baseDir: path.resolve(dirname, 'src') },
|
|
components: {
|
|
logout: { Button: '/components/Logout#MyLogout' },
|
|
},
|
|
}
|
|
```
|
|
|
|
### Default props every component receives
|
|
|
|
| Prop | Type | Notes |
|
|
|------|------|-------|
|
|
| `payload` | `Payload` | Full local API (server only) |
|
|
| `i18n` | `I18nClient` | Translation object |
|
|
|
|
Client components also get `clientConfig` via `useConfig()`.
|
|
|
|
## Key Slots — Root Components (`admin.components`)
|
|
|
|
| Slot | Type | Description |
|
|
|------|------|-------------|
|
|
| `graphics.Logo` | component | Full logo on login page |
|
|
| `graphics.Icon` | component | Small icon in nav |
|
|
| `Nav` | component | Replace entire sidebar/mobile nav |
|
|
| `header` | `[]` | Inject above Payload header (banners, alerts) |
|
|
| `actions` | `[]` | Buttons inside the header bar |
|
|
| `logout.Button` | component | Custom logout button |
|
|
| `beforeDashboard` | `[]` | Before default dashboard content |
|
|
| `afterDashboard` | `[]` | After default dashboard content |
|
|
| `beforeNavLinks` | `[]` | Before nav link list |
|
|
| `afterNavLinks` | `[]` | After nav link list |
|
|
| `beforeLogin` | `[]` | Before login form |
|
|
| `afterLogin` | `[]` | After login form |
|
|
| `settingsMenu` | `[]` | Gear-icon popup items above logout |
|
|
| `providers` | `[]` | React context providers wrapping entire panel |
|
|
| `views` | object | Replace/add full-page views |
|
|
|
|
## Dashboard Widgets (experimental)
|
|
|
|
```ts
|
|
admin: {
|
|
dashboard: {
|
|
widgets: [{
|
|
slug: 'user-stats',
|
|
Component: './components/UserStats.tsx#default',
|
|
fields: [{ name: 'title', type: 'text' }],
|
|
minWidth: 'small',
|
|
maxWidth: 'large',
|
|
}],
|
|
defaultLayout: ({ req }) => [
|
|
{ widgetSlug: 'collections', width: 'full' },
|
|
{ widgetSlug: 'user-stats', data: { title: 'Users' }, width: 'medium' },
|
|
],
|
|
},
|
|
}
|
|
```
|
|
|
|
Widget component receives `WidgetServerProps`:
|
|
|
|
```tsx
|
|
import type { WidgetServerProps } from 'payload'
|
|
|
|
export default async function UserStatsWidget({ req, widgetData }: WidgetServerProps) {
|
|
const count = await req.payload.count({ collection: 'users' })
|
|
return (
|
|
<div className="card">
|
|
<h3>{widgetData?.title ?? 'Users'}</h3>
|
|
<p style={{ fontSize: 32, fontWeight: 'bold' }}>{count.totalDocs}</p>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
`WidgetWidth` values: `'x-small' | 'small' | 'medium' | 'large' | 'x-large' | 'full'`
|
|
|
|
## Edit View Slots (`admin.components.edit` on Collection/Global)
|
|
|
|
| Slot | Notes |
|
|
|------|-------|
|
|
| `SaveButton` | Props: `SaveButtonServerProps` / `SaveButtonClientProps` |
|
|
| `SaveDraftButton` | Requires `versions.drafts: true` |
|
|
| `PublishButton` | Requires `versions.drafts: true` |
|
|
| `UnpublishButton` | Requires `versions.drafts: true` |
|
|
| `PreviewButton` | Requires `admin.preview` on collection |
|
|
| `Status` | Draft/published indicator |
|
|
| `beforeDocumentControls` | `[]` — before Save/Publish buttons |
|
|
| `editMenuItems` | `[]` — items in 3-dot menu dropdown |
|
|
| `Upload` | Collection-only; must integrate with Payload form system |
|
|
| `Description` | Shared with List View |
|
|
|
|
```tsx
|
|
// Custom save button — server component
|
|
import { SaveButton } from '@payloadcms/ui'
|
|
import type { SaveButtonServerProps } from 'payload'
|
|
|
|
export function MySaveButton(props: SaveButtonServerProps) {
|
|
return <SaveButton label="Save Draft" />
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
// editMenuItems — client component using PopupList
|
|
'use client'
|
|
import { PopupList } from '@payloadcms/ui'
|
|
import type { EditMenuItemsClientProps } from 'payload'
|
|
|
|
export const EditMenuItems = (props: EditMenuItemsClientProps) => (
|
|
<PopupList.ButtonGroup>
|
|
<PopupList.Button onClick={() => console.log(props.id)}>Custom Action</PopupList.Button>
|
|
</PopupList.ButtonGroup>
|
|
)
|
|
```
|
|
|
|
## List View Slots (`admin.components` on Collection)
|
|
|
|
| Slot | Notes |
|
|
|------|-------|
|
|
| `beforeList` | `[]` — before entire list |
|
|
| `afterList` | `[]` — after entire list |
|
|
| `beforeListTable` | `[]` — before the data table |
|
|
| `afterListTable` | `[]` — after the data table |
|
|
| `listMenuItems` | `[]` — items in list controls menu |
|
|
| `Description` | Shared with Edit View |
|
|
|
|
Replace entire List View:
|
|
|
|
```ts
|
|
admin: {
|
|
components: {
|
|
views: { list: { Component: '/path/to/MyListView' } },
|
|
},
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
// Keep built-in table, wrap with custom header
|
|
'use client'
|
|
import { DefaultListView } from '@payloadcms/ui'
|
|
import type { ListViewClientProps } from 'payload'
|
|
|
|
export function MyListView(props: ListViewClientProps) {
|
|
return (
|
|
<>
|
|
<h1>My Docs</h1>
|
|
<DefaultListView {...props} />
|
|
</>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Document Views (`views.edit[key]` on Collection/Global)
|
|
|
|
| Key | Description |
|
|
|-----|-------------|
|
|
| `root` | Takes over entire document route — no tabs/controls rendered |
|
|
| `default` | Primary edit view (Edit tab) |
|
|
| `versions` | Version history tab |
|
|
| `version` | Single version tab |
|
|
| `api` | REST JSON tab |
|
|
| `livePreview` | Live preview tab |
|
|
| `[custom key]` | Add new tab-based view |
|
|
|
|
```ts
|
|
// Add a custom tab to a collection
|
|
admin: {
|
|
components: {
|
|
views: {
|
|
edit: {
|
|
analytics: {
|
|
Component: '/components/AnalyticsView',
|
|
path: '/analytics',
|
|
tab: { label: 'Analytics', order: '50' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
Tab component props: `DocumentTabServerProps` / `DocumentTabClientProps`
|
|
|
|
## Custom Views (full-page)
|
|
|
|
```ts
|
|
// Root-level custom view
|
|
admin: {
|
|
components: {
|
|
views: {
|
|
myTool: {
|
|
Component: '/components/MyTool#MyTool',
|
|
path: '/my-tool',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
Custom views receive `AdminViewServerProps`: `initPageResult`, `params`, `searchParams`, `doc`, `payload`, `i18n`.
|
|
|
|
Use `DefaultTemplate` for built-in layout/nav:
|
|
|
|
```tsx
|
|
import { DefaultTemplate } from '@payloadcms/next/templates'
|
|
import type { AdminViewServerProps } from 'payload'
|
|
|
|
export function MyView({ initPageResult, params, searchParams }: AdminViewServerProps) {
|
|
if (!initPageResult.req.user) return <p>Not authorized</p>
|
|
|
|
return (
|
|
<DefaultTemplate
|
|
payload={initPageResult.req.payload}
|
|
permissions={initPageResult.permissions}
|
|
user={initPageResult.req.user}
|
|
i18n={initPageResult.req.i18n}
|
|
locale={initPageResult.locale}
|
|
params={params}
|
|
searchParams={searchParams}
|
|
visibleEntities={initPageResult.visibleEntities}
|
|
>
|
|
<h1>My Custom View</h1>
|
|
</DefaultTemplate>
|
|
)
|
|
}
|
|
```
|
|
|
|
**All custom views are public by default** — add auth check manually.
|
|
|
|
## Custom Providers
|
|
|
|
```ts
|
|
admin: {
|
|
components: {
|
|
providers: ['/components/MyProvider'],
|
|
},
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
'use client'
|
|
import React, { createContext, use } from 'react'
|
|
|
|
const Ctx = React.createContext<string>('')
|
|
|
|
export function MyProvider({ children }: { children: React.ReactNode }) {
|
|
return <Ctx value="hello">{children}</Ctx>
|
|
}
|
|
|
|
export const useMyCtx = () => use(Ctx)
|
|
```
|
|
|
|
Providers must be client components (`'use client'`). Wrap with a server component to pass server data in.
|
|
|
|
## Gotchas
|
|
|
|
- **Import path collisions:** In admin panel code always import from `@payloadcms/ui` (bare). In frontend code use deep imports like `@payloadcms/ui/elements/Button` for tree-shaking.
|
|
- **Non-serializable props:** `full config`, functions, class instances cannot cross the server/client boundary — use `clientProps` for serializable data only.
|
|
- `useField` on Lexical fields: `setValue` saves but doesn't re-render editor UI — use `dispatchFields` with `initialValue` too.
|
|
- `importMap.js` is regenerated every startup and on HMR — never edit manually.
|
|
- Custom views are public; add `if (!initPageResult.req.user) return <Redirect />` yourself.
|
|
- `Upload` custom component must use Payload's `<Upload>` from `@payloadcms/ui`, not a bare `<input type="file">`.
|
|
|
|
## Related
|
|
|
|
- [[wiki/payloadcms/admin|Admin Panel]]
|
|
- [[wiki/payloadcms/admin-panel-overview|Admin Panel Overview]]
|
|
- [[wiki/payloadcms/configuration|Configuration]]
|