| title | Swap in your own React components |
|---|---|
| label | Overview |
| order | 10 |
| desc | Fully customize your Admin Panel by swapping in your own React components. Add fields, remove views, update routes and change functions to sculpt your perfect Dashboard. |
| keywords | admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs |
The Payload Admin Panel is designed to be as minimal and straightforward as possible to allow for easy customization and full control over the UI. In order for Payload to support this level of customization, Payload provides a pattern for you to supply your own React components through your Payload Config.
All Custom Components in Payload are React Server Components by default. This enables the use of the Local API directly on the front-end. Custom Components are available for nearly every part of the Admin Panel for extreme granularity and control.
**Note:** Client Components continue to be fully supported. To use Client Components in your app, simply include the `'use client'` directive. Payload will automatically detect and remove all [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) default props before rendering your component. [More details](#client-components).There are four main types of Custom Components in Payload:
To swap in your own Custom Component, first determine the scope that corresponds to what you are trying to accomplish, consult the list of available components, then author your React component(s) accordingly.
As Payload compiles the Admin Panel, it checks your config for Custom Components. When detected, Payload either replaces its own default component with yours, or if none exists by default, renders yours outright. While there are many places where Custom Components are supported in Payload, each is defined in the same way using Component Paths.
To add a Custom Component, point to its file path in your Payload Config:
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
logout: {
Button: '/src/components/Logout#MyComponent', // highlight-line
},
},
},
})In order to ensure the Payload Config is fully Node.js compatible and as lightweight as possible, components are not directly imported into your config. Instead, they are identified by their file path for the Admin Panel to resolve on its own.
Component Paths, by default, are relative to your project's base directory. This is either your current working directory, or the directory specified in config.admin.importMap.baseDir.
Components using named exports are identified either by appending # followed by the export name, or using the exportName property. If the component is the default export, the # and export name can be omitted.
Both default exports and named exports are fully supported. Choose the pattern that works best for your codebase.
import { buildConfig } from 'payload'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const config = buildConfig({
// ...
admin: {
importMap: {
baseDir: path.resolve(dirname, 'src'), // highlight-line
},
components: {
logout: {
Button: '/components/Logout#MyComponent', // highlight-line
},
},
},
})In this example, we set the base directory to the src directory, and omit the /src/ part of our component path string.
Examples of component path syntax:
// Named export using hash syntax
Button: '/components/Logout#MyComponent'
// Default export (no hash needed)
Button: '/components/Logout'
// Named export using exportName property
Button: {
path: '/components/Logout',
exportName: 'MyComponent',
}While Custom Components are usually defined as a string, you can also pass in an object with additional options:
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
logout: {
// highlight-start
Button: {
path: '/src/components/Logout',
exportName: 'MyComponent',
},
// highlight-end
},
},
},
})The following options are available:
| Property | Description |
|---|---|
clientProps |
Props to be passed to the Custom Components if it's a Client Component. More details. |
exportName |
Instead of declaring named exports using # in the component path, you can also omit them from path and pass them in here. |
path |
File path to the Custom Component. Named exports can be appended to the end of the path, separated by a #. |
serverProps |
Props to be passed to the Custom Component if it's a Server Component. More details. |
For details on how to build Custom Components, see Building Custom Components.
In order for Payload to make use of Component Paths, an "Import Map" is automatically generated at either src/app/(payload)/admin/importMap.js or app/(payload)/admin/importMap.js. This file contains every Custom Component in your config, keyed to their respective paths. When Payload needs to lookup a component, it uses this file to find the correct import.
The Import Map is automatically regenerated at startup and whenever Hot Module Replacement (HMR) runs, or you can run payload generate:importmap to manually regenerate it.
The import map file is automatically regenerated in these scenarios:
- Application startup - Every time you start your development server or build your application
- Hot Module Replacement (HMR) - When you save changes to component files during development
- Manual generation - When you run
payload generate:importmaporpnpm payload generate:importmap
The import map is never regenerated during:
- Normal runtime (only at startup)
- After a production build completes
Using the config.admin.importMap.importMapFile property, you can override the location of the import map. This is useful if you want to place the import map in a different location, or if you want to use a custom file name.
import { buildConfig } from 'payload'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const config = buildConfig({
// ...
admin: {
importMap: {
baseDir: path.resolve(dirname, 'src'),
importMapFile: path.resolve(
dirname,
'app',
'(payload)',
'custom-import-map.js',
), // highlight-line
},
},
})If needed, custom items can be appended onto the Import Map. This is mostly only relevant for plugin authors who need to add a custom import that is not referenced in a known location.
To add a custom import to the Import Map, use the admin.dependencies property in your Payload Config:
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// ...
dependencies: {
myTestComponent: {
// myTestComponent is the key - can be anything
path: '/components/TestComponent.js#TestComponent',
type: 'component',
clientProps: {
test: 'hello',
},
},
},
},
})All Custom Components in Payload are React Server Components by default. This enables the use of the Local API directly on the front-end, among other things.
To make building Custom Components as easy as possible, Payload automatically provides common props, such as the payload class and the i18n object. This means that when building Custom Components within the Admin Panel, you do not have to get these yourself.
Here is an example:
import React from 'react'
import type { Payload } from 'payload'
async function MyServerComponent({
payload, // highlight-line
}: {
payload: Payload
}) {
const page = await payload.findByID({
collection: 'pages',
id: '123',
})
return <p>{page.title}</p>
}Each Custom Component receives the following props by default:
| Prop | Description |
|---|---|
payload |
The Payload class. |
i18n |
The i18n object. |
It is also possible to pass custom props to your Custom Components. To do this, you can use either the clientProps or serverProps properties depending on whether your prop is serializable, and whether your component is a Server or Client Component.
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
// highlight-line
components: {
logout: {
Button: {
path: '/src/components/Logout#MyComponent',
clientProps: {
myCustomProp: 'Hello, World!', // highlight-line
},
},
},
},
},
})Here is how your component might receive this prop:
import React from 'react'
import { Link } from '@payloadcms/ui'
export function MyComponent({ myCustomProp }: { myCustomProp: string }) {
return <Link href="/admin/logout">{myCustomProp}</Link>
}All Custom Components in Payload are React Server Components by default, however, it is possible to use Client Components by simply adding the 'use client' directive at the top of your file. Payload will automatically detect and remove all non-serializable default props before rendering your component.
// highlight-start
'use client'
// highlight-end
import React, { useState } from 'react'
export function MyClientComponent() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
)
}From any Server Component, the Payload Config can be accessed directly from the payload prop:
import React from 'react'
export default async function MyServerComponent({
payload: {
config, // highlight-line
},
}) {
return <Link href={config.serverURL}>Go Home</Link>
}But, the Payload Config is non-serializable by design. It is full of custom validation functions and more. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components.
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the useConfig hook:
'use client'
import React from 'react'
import { useConfig } from '@payloadcms/ui'
export function MyClientComponent() {
// highlight-start
const {
config: { serverURL },
} = useConfig()
// highlight-end
return <Link href={serverURL}>Go Home</Link>
}Similarly, all Field Components automatically receive their respective Field Config through props.
Within Server Components, this prop is named field:
import React from 'react'
import type { TextFieldServerComponent } from 'payload'
export const MyClientFieldComponent: TextFieldServerComponent = ({
field: { name },
}) => {
return <p>{`This field's name is ${name}`}</p>
}Within Client Components, this prop is named clientField because its non-serializable props have been removed:
'use client'
import React from 'react'
import type { TextFieldClientComponent } from 'payload'
export const MyClientFieldComponent: TextFieldClientComponent = ({
clientField: { name },
}) => {
return <p>{`This field's name is ${name}`}</p>
}All Custom Components can support language translations to be consistent with Payload's I18n. This will allow your Custom Components to display the correct language based on the user's preferences.
To do this, first add your translation resources to the I18n Config. Then from any Server Component, you can translate resources using the getTranslation function from @payloadcms/translations.
All Server Components automatically receive the i18n object as a prop by default:
import React from 'react'
import { getTranslation } from '@payloadcms/translations'
export default async function MyServerComponent({ i18n }) {
const translatedTitle = getTranslation(myTranslation, i18n) // highlight-line
return <p>{translatedTitle}</p>
}The best way to do this within a Client Component is to import the useTranslation hook from @payloadcms/ui:
'use client'
import React from 'react'
import { useTranslation } from '@payloadcms/ui'
export function MyClientComponent() {
const { t, i18n } = useTranslation() // highlight-line
return (
<ul>
<li>{t('namespace1:key', { variable: 'value' })}</li>
<li>{t('namespace2:key', { variable: 'value' })}</li>
<li>{i18n.language}</li>
</ul>
)
}All Custom Views can support multiple locales to be consistent with Payload's Localization feature. This can be used to scope API requests, etc.
All Server Components automatically receive the locale object as a prop by default:
import React from 'react'
export default async function MyServerComponent({ payload, locale }) {
const localizedPage = await payload.findByID({
collection: 'pages',
id: '123',
locale,
})
return <p>{localizedPage.title}</p>
}The best way to do this within a Client Component is to import the useLocale hook from @payloadcms/ui:
'use client'
import React from 'react'
import { useLocale } from '@payloadcms/ui'
function Greeting() {
const locale = useLocale() // highlight-line
const trans = {
en: 'Hello',
es: 'Hola',
}
return <span>{trans[locale.code]}</span>
}To make it easier to build your Custom Components, you can use Payload's built-in React Hooks in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs.
'use client'
import React from 'react'
import { useDocumentInfo } from '@payloadcms/ui'
export function MyClientComponent() {
const { slug } = useDocumentInfo() // highlight-line
return <p>{`Entity slug: ${slug}`}</p>
}Payload has a robust CSS Library that you can use to style your Custom Components to match to Payload's built-in styling. This will ensure that your Custom Components integrate well into the existing design system. This will make it so they automatically adapt to any theme changes that might occur.
To apply custom styles, simply import your own .css or .scss file into your Custom Component:
import './index.scss'
export function MyComponent() {
return <div className="my-component">My Custom Component</div>
}Then to colorize your Custom Component's background, for example, you can use the following CSS:
.my-component {
background-color: var(--theme-elevation-500);
}Payload also exports its SCSS library for reuse which includes mixins, etc. To use this, simply import it as follows into your .scss file:
@import '~@payloadcms/ui/scss';
.my-component {
@include mid-break {
background-color: var(--theme-elevation-900);
}
}An often overlooked aspect of Custom Components is performance. If unchecked, Custom Components can lead to slow load times of the Admin Panel and ultimately a poor user experience.
This is different from front-end performance of your public-facing site.
For more performance tips, see the [Performance documentation](../performance/overview).All Custom Components are built using React. For this reason, it is important to follow React best practices. This includes using memoization, streaming, caching, optimizing renders, using hooks appropriately, and more.
To learn more, see the React documentation.
The Admin Panel itself is a Next.js application. For this reason, it is also important to follow Next.js best practices. This includes bundling, when to use layouts vs pages, where to place the server/client boundary, and more.
To learn more, see the Next.js documentation.
With Server Components, be aware of what is being sent to through the server/client boundary. All props are serialized and sent through the network. This can lead to large HTML sizes and slow initial load times if too much data is being sent to the client.
To minimize this, you must be explicit about what props are sent to the client. Prefer server components and only send the necessary props to the client. This will also offset some of the JS execution to the server.
**Tip:** Use [React Suspense](https://react.dev/reference/react/Suspense) to progressively load components and improve perceived performance.If subscribing your component to form state, it may be re-rendering more often than necessary.
To do this, use the useFormFields hook instead of useFields when you only need to access specific fields.
'use client'
import { useFormFields } from '@payloadcms/ui'
const MyComponent: TextFieldClientComponent = ({ path }) => {
const value = useFormFields(([fields, dispatch]) => fields[path])
// ...
}When building Custom Components it's important to be mindful of your bundle sizes sent to the client which are primarily affected by your imports. Generally speaking you can import third party packages as needed though it's best to avoid large packages that bloat your bundle size.
The most common issue is importing from our @payloadcms/ui package in the wrong context. So here are the simple rules to follow:
- In the admin panel UI you always want to import everything from
@payloadcms/uito ensure there's no package mismatch issues. - In the frontend application you must always import components and utilities from
@payloadcms/ui/path/tofor exampleimport { Button } from '@payloadcms/ui/elements/Button'to ensure tree shaking is effective and your bundle sizes are minimized otherwise it will include the entire library with your frontend code and greatly bloat your bundle size.
There's a few variations of this error that hint at the same problem, sometimes it will error on useConfig hook or any other Payload UI hook like useAuth, useLocale with the error message that the value being destructured is undefined.
See Troubleshooting Common Issues in Payload for more details on resolving dependency mismatches.
Generally we recommend pinning Payload packages to the exact same version in order to ensure that your package manager installs the same versions across all Payload packages.