Swap in your own React components

The Payload Admin Panel is designed to be as minimal and straightforward as possible to allow for both 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, with the exception of Custom Providers. 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.

There are four main types of Custom Components in Payload:

To swap in your own Custom Component, consult the list of available components. Determine the scope that corresponds to what you are trying to accomplish, then author your React component(s) accordingly.

Defining Custom Components in the Payload Config

In the Payload Config, you can define custom React Components to enhance the admin interface. However, these components should not be imported directly into the server-only Payload Config to avoid including client-side code. Instead, you specify the path to the component. Here’s how you can do it:

src/components/Logout.tsx

1
'use client'
2
import React from 'react'
3
4
export const MyComponent = () => {
5
return (
6
<button>Click me!</button>
7
)
8
}

payload.config.ts:

1
import { buildConfig } from 'payload'
2
3
const config = buildConfig({
4
// ...
5
admin: {
6
components: {
7
logout: {
8
Button: '/src/components/Logout#MyComponent'
9
}
10
}
11
},
12
})

In the path /src/components/Logout#MyComponent, /src/components/Logout is the file path, and MyComponent is the named export. If the component is the default export, the export name can be omitted. Path and export name are separated by a #.

Configuring the Base Directory

Component paths, by default, are relative to your working directory - this is usually where your Next.js config lies. To simplify component paths, you have the option to configure the base directory using the admin.baseDir.baseDir property:

1
import { buildConfig } from 'payload'
2
import { fileURLToPath } from 'node:url'
3
import path from 'path'
4
const filename = fileURLToPath(import.meta.url)
5
const dirname = path.dirname(filename)
6
7
const config = buildConfig({
8
// ...
9
admin: {
10
importMap: {
11
baseDir: path.resolve(dirname, 'src'),
12
},
13
components: {
14
logout: {
15
Button: '/components/Logout#MyComponent'
16
}
17
}
18
},
19
})

In this example, we set the base directory to the src directory - thus we can omit the /src/ part of our component path string.

Passing Props

Each React Component in the Payload Config is typed as PayloadComponent. This usually is a string, but can also be an object containing the following properties:

PropertyDescription
clientPropsProps to be passed to the React Component if it's a Client Component
exportNameInstead of declaring named exports using # in the component path, you can also omit them from path and pass them in here.
pathPath to the React Component. Named exports can be appended to the end of the path, separated by a #
serverPropsProps to be passed to the React Component if it's a Server Component

To pass in props from the config, you can use the clientProps and/or serverProps properties. This alleviates the need to use an HOC (Higher-Order-Component) to declare a React Component with props passed in.

Here is an example:

src/components/Logout.tsx

1
'use client'
2
import React from 'react'
3
4
export const MyComponent = ({ text }: { text: string }) => {
5
return (
6
<button>Click me! {text}</button>
7
)
8
}

payload.config.ts:

1
import { buildConfig } from 'payload'
2
3
const config = buildConfig({
4
// ...
5
admin: {
6
components: {
7
logout: {
8
Button: {
9
path: '/src/components/Logout',
10
clientProps: {
11
text: 'Some Text.'
12
},
13
exportName: 'MyComponent'
14
}
15
}
16
}
17
},
18
})

Import Maps

It's essential to understand how PayloadComponent paths function behind the scenes. Directly importing React Components into your Payload Config using import statements can introduce client-only modules like CSS into your server-only config. This could error when attempting to load the Payload Config in server-only environments and unnecessarily increase the size of the Payload Config, which should remain streamlined and efficient for server use.

Instead, we utilize component paths to reference React Components. This method enhances the Payload Config with actual React Component imports on the client side, without affecting server-side usage. A script is deployed to scan the Payload Config, collecting all component paths and creating an importMap.js. This file, located in app/(payload)/admin/importMap.js, must be statically imported by your Next.js root page and layout. The script imports all the React Components from the specified paths into a Map, associating them with their respective paths (the ones you defined).

When constructing the ClientConfig, Payload uses the component paths as keys to fetch the corresponding React Component imports from the Import Map. It then substitutes the PayloadComponent with a MappedComponent. A MappedComponent includes the React Component and additional metadata, such as whether it's a server or a client component and which props it should receive. These components are then rendered through the <RenderComponent /> component within the Payload Admin Panel.

Import maps are regenerated whenever you modify any element related to component paths. This regeneration occurs at startup and whenever Hot Module Replacement (HMR) runs. If the import maps fail to regenerate during HMR, you can restart your application and execute the payload generate:importmap command to manually create a new import map. If you encounter any errors running this command, see the Troubleshooting section.

Component paths in external packages

Component paths are resolved relative to your project's base directory, which is either your current working directory or the directory specified in config.admin.baseDir. When using custom components from external packages, you can't use relative paths. Instead, use an import path that's accessible as if you were writing an import statement in your project's base directory.

For example, to export a field with a custom component from an external package named my-external-package:

1
import type { Field } from 'payload'
2
export const MyCustomField: Field = {
3
type: 'text',
4
name: 'MyField',
5
admin: {
6
components: {
7
Field: 'my-external-package/client#MyFieldComponent'
8
}
9
}
10
}

Despite MyFieldComponent living in src/components/MyFieldComponent.tsx in my-external-package, this will not be accessible from the consuming project. Instead, we recommend exporting all custom components from one file in the external package. For example, you can define a src/client.ts file in my-external-package`:

1
'use client'
2
export { MyFieldComponent } from './components/MyFieldComponent'

Then, update the package.json of `my-external-package:

1
{
2
...
3
"exports": {
4
"./client": {
5
"import": "./dist/client.js",
6
"types": "./dist/client.d.ts",
7
"default": "./dist/client.js"
8
}
9
}
10
}

This setup allows you to specify the component path as my-external-package/client#MyFieldComponent as seen above. The import map will generate:

1
import { MyFieldComponent } from 'my-external-package/client'

which is a valid way to access MyFieldComponent that can be resolved by the consuming project.

Custom Components from unknown locations

By default, any component paths from known locations are added to the import map. However, if you need to add any components from unknown locations to the import map, you can do so by adding them to the admin.dependencies array in your Payload Config. This is mostly only relevant for plugin authors and not for regular Payload users.

Example:

1
export default {
2
// ...
3
admin: {
4
// ...
5
dependencies: {
6
myTestComponent: { // myTestComponent is the key - can be anything
7
path: '/components/TestComponent.js#TestComponent',
8
type: 'component',
9
clientProps: {
10
test: 'hello',
11
},
12
},
13
},
14
}
15
}

This way, TestComponent is added to the import map, no matter if it's referenced in a known location or not. On the client, you can then use the component like this:

1
'use client'
2
3
import { RenderComponent, useConfig } from '@payloadcms/ui'
4
import React from 'react'
5
6
export const CustomView = () => {
7
const { config } = useConfig()
8
return (
9
<div>
10
<RenderComponent mappedComponent={config.admin.dependencies?.myTestComponent} />
11
</div>
12
)
13
}

Root Components

Root Components are those that effect the Admin Panel generally, such as the logo or the main nav.

To override Root Components, use the admin.components property in your Payload Config:

1
import { buildConfig } from 'payload'
2
3
export default buildConfig({
4
// ...
5
admin: {
6
components: {
7
// ...
8
},
9
},
10
})

For details on how to build Custom Components, see Building Custom Components.

The following options are available:

PathDescription
NavContains the sidebar / mobile menu in its entirety.
beforeNavLinksAn array of Custom Components to inject into the built-in Nav, before the links themselves.
afterNavLinksAn array of Custom Components to inject into the built-in Nav, after the links.
beforeDashboardAn array of Custom Components to inject into the built-in Dashboard, before the default dashboard contents.
afterDashboardAn array of Custom Components to inject into the built-in Dashboard, after the default dashboard contents.
beforeLoginAn array of Custom Components to inject into the built-in Login, before the default login form.
afterLoginAn array of Custom Components to inject into the built-in Login, after the default login form.
logout.ButtonThe button displayed in the sidebar that logs the user out.
graphics.IconThe simplified logo used in contexts like the the Nav component.
graphics.LogoThe full logo used in contexts like the Login view.
providersCustom React Context providers that will wrap the entire Admin Panel. More details.
actionsAn array of Custom Components to be rendered in the header of the Admin Panel, providing additional interactivity and functionality.
viewsOverride or create new views within the Admin Panel. More details.

Custom Providers

As you add more and more Custom Components to your Admin Panel, you may find it helpful to add additional React Context(s). Payload allows you to inject your own context providers in your app so you can export your own custom hooks, etc.

To add a Custom Provider, use the admin.components.providers property in your Payload Config:

1
import { buildConfig } from 'payload'
2
3
export default buildConfig({
4
// ...
5
admin: {
6
components: {
7
providers: ['/path/to/MyProvider'],
8
},
9
},
10
})

Then build your Custom Provider as follows:

1
'use client'
2
import React, { createContext, useContext } from 'react'
3
4
const MyCustomContext = React.createContext(myCustomValue)
5
6
export const MyProvider: React.FC = ({ children }) => {
7
return (
8
<MyCustomContext.Provider value={myCustomValue}>
9
{children}
10
</MyCustomContext.Provider>
11
)
12
}
13
14
export const useMyCustomContext = () => useContext(MyCustomContext)

Building Custom Components

All Custom Components in Payload are React Server Components by default, with the exception of Custom Providers. 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:

1
import React from 'react'
2
3
const MyServerComponent = async ({
4
payload
5
}) => {
6
const page = await payload.findByID({
7
collection: 'pages',
8
id: '123',
9
})
10
11
return (
12
<p>{page.title}</p>
13
)
14
}

Each Custom Component receives the following props by default:

PropDescription
payloadThe Payload class.
i18nThe i18n object.

Custom Components also receive various other props that are specific to the context in which the Custom Component is being rendered. For example, Custom Views receive the user prop. For a full list of available props, consult the documentation related to the specific component you are working with.

Client Components

When Building Custom Components, it's still possible to use client-side code such as useState or the window object. To do this, simply add the use client directive at the top of your file. Payload will automatically detect and remove all default, non-serializable props before rendering your component.

1
'use client'
2
import React, { useState } from 'react'
3
4
export const MyClientComponent: React.FC = () => {
5
const [count, setCount] = useState(0)
6
7
return (
8
<button onClick={() => setCount(count + 1)}>
9
Clicked {count} times
10
</button>
11
)
12
}

Accessing the Payload Config

From any Server Component, the Payload Config can be accessed directly from the payload prop:

1
import React from 'react'
2
3
export default async function MyServerComponent({
4
payload: {
5
config
6
}
7
}) {
8
return (
9
<Link href={config.serverURL}>
10
Go Home
11
</Link>
12
)
13
}

But, the Payload Config is non-serializable by design. It is full of custom validation functions, React components, etc. 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:

1
import React from 'react'
2
import { useConfig } from '@payloadcms/ui'
3
4
export const MyClientComponent: React.FC = () => {
5
const { config: { serverURL } } = useConfig()
6
7
return (
8
<Link href={serverURL}>
9
Go Home
10
</Link>
11
)
12
}

All Field Components automatically receive their respective Field Config through a common field prop:

1
'use client'
2
import React from 'react'
3
import type { TextFieldClientComponent } from 'payload'
4
5
export const MyClientFieldComponent: TextFieldClientComponent = ({ field: { name } }) => {
6
return (
7
<p>
8
{`This field's name is ${name}`}
9
</p>
10
)
11
}

Using Hooks

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:

1
'use client'
2
import React from 'react'
3
import { useDocumentInfo } from '@payloadcms/ui'
4
5
export const MyClientComponent: React.FC = () => {
6
const { slug } = useDocumentInfo()
7
8
return (
9
<p>{`Entity slug: ${slug}`}</p>
10
)
11
}

Getting the Current Language

All Custom Components can support multiple languages to be consistent with Payload's Internationalization. To do this, first add your translation resources to the I18n Config.

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.

1
import React from 'react'
2
import { getTranslation } from '@payloadcms/translations'
3
4
export default async function MyServerComponent({ i18n }) {
5
const translatedTitle = getTranslation(myTranslation, i18n)
6
7
return (
8
<p>{translatedTitle}</p>
9
)
10
}

The best way to do this within a Client Component is to import the useTranslation hook from @payloadcms/ui:

1
import React from 'react'
2
import { useTranslation } from '@payloadcms/ui'
3
4
export const MyClientComponent: React.FC = () => {
5
const { t, i18n } = useTranslation()
6
7
return (
8
<ul>
9
<li>{t('namespace1:key', { variable: 'value' })}</li>
10
<li>{t('namespace2:key', { variable: 'value' })}</li>
11
<li>{i18n.language}</li>
12
</ul>
13
)
14
}

Getting the Current Locale

All Custom Views can support multiple locales to be consistent with Payload's Localization. They automatically receive the locale object as a prop by default. This can be used to scope API requests, etc.:

1
import React from 'react'
2
3
export default async function MyServerComponent({ payload, locale }) {
4
const localizedPage = await payload.findByID({
5
collection: 'pages',
6
id: '123',
7
locale,
8
})
9
10
return (
11
<p>{localizedPage.title}</p>
12
)
13
}

The best way to do this within a Client Component is to import the useLocale hook from @payloadcms/ui:

1
import React from 'react'
2
import { useLocale } from '@payloadcms/ui'
3
4
const Greeting: React.FC = () => {
5
const locale = useLocale()
6
7
const trans = {
8
en: 'Hello',
9
es: 'Hola',
10
}
11
12
return (
13
<span>{trans[locale.code]}</span>
14
)
15
}

Styling Custom Components

Payload has a robust CSS Library that you can use to style your Custom Components similarly to Payload's built-in styling. This will ensure that your Custom Components match the existing design system, and so that 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:

1
import './index.scss'
2
3
export const MyComponent: React.FC = () => {
4
return (
5
<div className="my-component">
6
My Custom Component
7
</div>
8
)
9
}

Then to colorize your Custom Component's background, for example, you can use the following CSS:

1
.my-component {
2
background-color: var(--theme-elevation-500);
3
}

Payload also exports its SCSS library for reuse which includes mixins, etc. To use this, simply import it as follows into your .scss file:

1
@import '~payload/scss';
2
3
.my-component {
4
@include mid-break {
5
background-color: var(--theme-elevation-900);
6
}
7
}
Next

Customizing Views