Customize the Payload admin
Your editors work in Payload’s own admin, with the Evolve pages and blocks in it. You change it in three ways, each in its own place:
| To | Add | In |
|---|---|---|
| Change the config: collections, fields, storage | a plugin | packages/payload-config |
| Show your own UI in the admin | a component | frontend/payload-admin/src/admin |
| Make it look like your brand | a logo and CSS | frontend/payload-admin |
The config is one module that two apps load: the admin app, and your CMS
service with PayloadModule. Components are named in it by path, so only the
admin app loads them.
flowchart LR config["packages/payload-config<br/>payload.config.ts"] --> admin["frontend/payload-admin<br/>the admin app"] config --> service["your CMS service<br/>PayloadModule"] components["frontend/payload-admin/src/admin<br/>components and CSS"] --> admin
This page assumes Payload is your CMS. See Add a CMS.
1. Add a plugin
Section titled “1. Add a plugin”A Payload plugin is a function that takes the config and returns a new one. This one puts every Evolve collection under one heading in the sidebar:
import { evolveCms } from "@evolve-framework/payload/config";import { buildConfig, type Config, type Plugin } from "payload";
const groupCollections: Plugin = (incoming: Config): Config => ({ ...incoming, collections: (incoming.collections ?? []).map((collection) => collection.slug === "users" ? collection : { ...collection, admin: { ...collection.admin, group: "Content" } }, ),});
export default buildConfig({ // ...database adapter, editor, and secret plugins: [...evolveCms(), groupCollections],});- Put yours after
evolveCms(). Payload runs the plugins in order, so yours sees the collections Evolve registers. - Spread
evolveCms(). It returns a list of plugins. Payload collects the list before it runs any of them, so a plugin added from inside another plugin never runs.
An official Payload plugin goes in the same list. For example, keep media in S3 instead of on the admin’s disk:
pnpm --filter ./packages/payload-config add @payloadcms/storage-s3import { s3Storage } from "@payloadcms/storage-s3";
export default buildConfig({ plugins: [ ...evolveCms(), s3Storage({ collections: { media: true }, bucket: process.env.S3_BUCKET ?? "", config: { region: process.env.S3_REGION }, }), ],});Install the same version as payload. Payload refuses to start with mixed
versions of its packages.
2. Add your own component
Section titled “2. Add your own component”This one replaces a text field with your brand colors as swatches.
Write it
Section titled “Write it”"use client";
import { FieldLabel, useField } from "@payloadcms/ui";import type { TextFieldClientProps } from "payload";
const colors = { brand: "#0b5fff", sand: "#e9dcc9", ink: "#1b1f24" };
export function BrandColorField({ path, field }: TextFieldClientProps) { const { value, setValue } = useField<string>({ path });
return ( <div className="field-type text"> <FieldLabel label={field?.label} path={path} required={field?.required} /> <div className="brand-colors"> {Object.entries(colors).map(([name, hex]) => ( <button key={name} type="button" aria-label={name} aria-pressed={value === name} className="brand-colors__swatch" style={{ background: hex }} onClick={() => setValue(name)} /> ))} </div> </div> );}The field still stores a plain string, so your mapper and the GraphQL API see no difference.
Point a field at it
Section titled “Point a field at it”Name the component by path in the field’s admin.components. A path that
starts with / is relative to frontend/payload-admin.
payload: { slug: "quote", fields: [ { name: "quote", type: "textarea", required: true }, { name: "background", type: "text", admin: { components: { Field: "/src/admin/brand-color-field#BrandColorField" }, }, }, ],},This is the Payload config of a block of your own. See Add a content block.
Regenerate the import map
Section titled “Regenerate the import map”pnpm --filter ./frontend/payload-admin generate:importmapPayload adds the component to src/app/(payload)/admin/importMap.js. Commit
that file, and run the command again whenever you add, move, or rename a
component.
More places for a component
Section titled “More places for a component”| Option | Where it shows |
|---|---|
admin.components.Field on a field |
In place of the field’s input |
admin.components.beforeDashboard |
Above the collections on the dashboard |
admin.components.afterNavLinks |
Below the links in the sidebar |
admin.components.beforeLogin |
Above the login form |
admin.components.graphics.Logo |
On the login screen |
admin.components.graphics.Icon |
In the navigation |
The admin.components options without a field go at the top level of your
Payload config. See Payload’s
custom components for
all of them.
3. Make it your brand
Section titled “3. Make it your brand”Logo and title
Section titled “Logo and title”export function Logo() { return <img src="/brand/logo.svg" alt="Acme" width={180} height={40} />;}
export function Icon() { return <img src="/brand/icon.svg" alt="" width={24} height={24} />;}Put the two images in frontend/payload-admin/public/brand/. Then name the
components in the config, and run generate:importmap again:
export default buildConfig({ admin: { meta: { titleSuffix: " · Acme CMS", icons: [{ rel: "icon", url: "/brand/icon.svg" }], }, components: { graphics: { Logo: "/src/admin/brand#Logo", Icon: "/src/admin/brand#Icon", }, }, }, // ...});Colors and fonts
Section titled “Colors and fonts”The admin app imports custom.css after Payload’s own stylesheet. Payload’s
styles sit in a cascade layer, so your rules win without !important.
:root { --font-body: "Inter", system-ui, sans-serif; --brand: #0b5fff; --brand-hover: #0947c2; --brand-text: #ffffff;}
html[data-theme="dark"] { --brand: #8fb0ff; --brand-hover: #b5cbff; --brand-text: #0b1020;}
/* Save, Publish, Create new */.btn--style-primary { --bg-color: var(--brand); --hover-bg: var(--brand-hover); --color: var(--brand-text);}
.brand-colors { display: flex; gap: calc(var(--base) * 0.4);}
.brand-colors__swatch { width: 2rem; height: 2rem; border: 1px solid var(--theme-elevation-150); border-radius: var(--style-radius-s); cursor: pointer;}
.brand-colors__swatch[aria-pressed="true"] { outline: 2px solid var(--theme-elevation-800); outline-offset: 2px;}- Both themes. Payload sets
data-themeonhtml. Color your own components with its--theme-elevation-*variables and they follow the editor’s theme. To offer only one theme, setadmin.theme: "light". - Your font.
--font-bodyonly names it. Load it the way any Next.js app does, for example withnext/fontin the layout. - Payload’s class names. Classes such as
.btn--style-primarybelong to Payload. Check your overrides when you upgrade it.
See also
Section titled “See also”- Add a content block: your own block, from the CMS to the storefront
evolveCms: every option of the Evolve plugin- Payload’s plugins and admin docs

