payload-theme-kit
v0.1.0
Published
A Payload CMS admin theme plugin — branding, grouped sidebar nav, card-styled sections, and a modernized edit view, all via Payload's own customization points.
Maintainers
Readme
payload-theme-kit
Reusable branding, grouped sidebar navigation, and card-styled field sections for the Payload CMS admin panel. Payload has no official theme system — this plugin covers the common white-labeling needs (logo, brand color, grouped nav, nicer settings pages, a modernized edit view) via Payload's own supported customization points, without reimplementing any of its form, validation, or save logic.
Install
pnpm add payload-theme-kitPeer dependencies: payload (^3.0.0), @payloadcms/ui (^3.0.0),
@payloadcms/next (^3.0.0), react/react-dom (^19.0.0), next
(^15.0.0 || ^16.0.0) — matching whatever your project already uses.
Usage
// payload.config.ts
import { themeKit } from "payload-theme-kit";
export default buildConfig({
// ...
plugins: [
themeKit({
branding: {
logoText: "Acme",
primaryColor: "#f29520",
},
nav: {
groups: {
Content: ["pages", "posts"],
Applications: ["submissions", "leads"],
},
},
cardSections: {
settings: {
"Basic Information": "Globe",
"Social Links": "Share2",
},
},
pageHeader: {
settings: {
icon: "Settings",
title: "Site Settings",
subtitle: "Manage global configuration",
},
},
flattenArrays: {
settings: { socialLinks: "platform" },
},
sidebar: {
tagline: "Powered by Acme",
roleLabel: "Admin",
},
booleanCells: {
posts: ["isPublished"],
},
}),
],
});Every option is independent and opt-in — nothing not explicitly configured is touched, and everything else is left as Payload's stock default.
After adding the plugin (or after adding a new option that registers a
component path the first time — e.g. your first cardSections or
pageHeader entry), run your app's Payload import-map generation once so
that component path resolves. This is triggered automatically by
@payloadcms/next's dev/build pipeline, or manually via
payload generate:importmap.
Options
branding
logoText?: string— text logo shown in the nav and login screen.logoImageUrl?: string— image logo; takes precedence overlogoText.primaryColor?: string— a single hex color, expanded into a full--color-success-{50..950}ramp anchored at 500 — Payload's accent color for buttons, active nav items, focus rings, and icon badges. This is the recommended way to set a brand color. Overriding a single CSS variable viacssVariablesisn't enough on its own, since Payload's--theme- success-*tokens each derive from a different raw ramp stop depending on light/dark mode — a full generated ramp is the only way to get consistent theming in both modes.cssVariables?: Record<string, string>— raw CSS custom property overrides, applied at:rootinside the admin panel, merged on top of (and overriding individual stops from)primaryColor's generated ramp. Use Payload's real tokens directly, e.g.--color-base-900,--color-error-500,--color-warning-500— see@payloadcms/ui/dist/scss/colors.scssin your installed version for the full list.
nav
groups: Record<string, string[]>— sidebar group label mapped to the collection/globalslugs that belong under it. Sets each item'sadmin.group. Payload always renders its own default "Collections" and "Globals" groups first, ahead of any custom group — this plugin automatically pushes the default "Globals" group (in the default English locale) below all custom groups, since it's usually the least useful one to see first.
cardSections
Record<collectionOrGlobalSlug, Record<collapsibleFieldLabel, lucideIconName>>For each
type: 'collapsible'field in the target collection/global whoselabelmatches a key you provide, the plugin swaps in a custom label (icon + title) and scoped CSS that turns Payload's default collapsible into a bordered, shadowed card. Onlycollapsiblefields are affected — deliberately notgroupfields, sincegroupnests the field's data in your document (breaking existing data/consumers), whilecollapsibleis UI-only. Icon names are lucide-react PascalCase export names (e.g."Globe","Share2") — resolved at runtime vialucide-react/dynamic.
pageHeader
Record<globalSlug, { icon: string; title: string; subtitle?: string }>Replaces a global's default plain-text title with an icon badge + title + optional subtitle banner, rendered in Payload's own
admin.components.elements.Descriptionslot right below the (now hidden) default title. Collections aren't supported — they don't have a fixed single title to replace the same way. Icon names use the same lucide-react convention ascardSections.
flattenArrays
Record<collectionOrGlobalSlug, Record<arrayFieldName, subFieldName>>Hides an array field's default header/"Collapse All" toolbar and drag handle (each row's own collapse toggle is left alone), and labels each row using the value of one of its own sub-fields instead of Payload's default "Item 01" placeholder. For example,
{ settings: { socialLinks: "platform" } }labels eachsocialLinksrow with its ownplatformfield's value.
sidebar
tagline?: string— small text shown under the logo in a branded panel above the nav (which becomes a clickable link to the dashboard).roleLabel?: string— static label shown next to the signed-in user's email in a footer panel below the nav.Setting either of these registers both the brand panel and the user footer; there's currently no way to enable just one.
booleanCells
Record<collectionOrGlobalSlug, string[]>— checkbox fieldnames to render as a toggle-switch pill in the collection list/table view, instead of Payload's default plain "true"/"false" text. The toggle is directly clickable there too — clicking it PATCHes the field via Payload's REST API and refreshes the row, with no need to open the document. Requires the signed-in user to have update access on the field; Payload's own access control is enforced server-side regardless of this UI.The edit-form checkbox input itself is always restyled as a toggle switch globally, for every checkbox field, regardless of this option — that's a pure CSS swap with no field-data risk.
booleanCellsis the explicit opt-in needed only for the list-view cell, since coloring it by true/false requires a customCellcomponent, not just CSS.
Design notes
- No custom edit-view replacement: Payload's per-global
admin.components.views.editslot can fully replace a global's edit view, but that means reimplementing form state, validation, save, and draft/versions UI. This plugin only swaps a field'sLabel/RowLabel/Cellcomponent and adds scoped CSS — 100% of Payload's default save/ validation path stays intact everywhere. - The document edit view's chrome (tabs, save button, sidebar panel, input styling, focus rings) is modernized globally via CSS, for every collection/global, with no config needed — same approach as the collection list table styling.
- Styles are inlined as JS string constants rendered via a
<style>tag inside anadmin.components.providersentry, not shipped as a real.cssfile — so consuming Next.js apps never need to add this package totranspilePackages.
