Theme
Theme is required. The admin nav shows one Global (theme) in admin.group: "Theme". Colors, Typography, Appearance, and Identity are field tabs on that document. Save writes the whole form. Theme has no draft mode and no preview pane. Contrast is advice at 7:1 in the Needs-attention modal; it is never enforced. Features does not install or remove Theme. Who may edit it is one Roles grant, Designer in the seed, and that same grant opens brand-assets.
bison.config.json is the seed a site starts from, not the source of truth. Pass destructive (or seed.brandDestructive); otherwise Theme uses the library’s #FF7C6B. Success falls back to #83CEA2. Those defaults meet the 7:1 fill-label target; a site that picks a mid-tone green or red still can, and the color card shows Needs attention. Changing a brand hex or This color is leaves the chips as they were; Regenerate scale rewrites the ramp so it passes through that hex at the named step, from 50 through 950 including 500. A near-black or near-white hex does not tint the other steps. Success and Destructive family banners use the same scale steps as the visitor Alert (50 / 300 / 900 / 600 in Light; 900 at 60% / 700 / 100 / 300 in Dark).
-
Register the Global.
updatedefaults tocanUseFeature("theme"). Passaccess.readto tighten who can read the document. Omittingaccessstill registers the Global.import {BRAND_ASSETS_SLUG,createBrandAssets,createFeatures,createRoles,createTheme,} from "@bison-lab/payload-core";import bisonConfig from "../bison.config.json";export default buildConfig({collections: [createBrandAssets()],globals: [createRoles(),createFeatures(),...createTheme({seed: bisonConfig,logo: { collection: BRAND_ASSETS_SLUG },identity: {fallback: {lockup: { url: "/logo-lockup.svg", alt: "Acme" },mark: { url: "/logo-mark.svg", alt: "Acme mark" },},},colorUsages: true,onPublish: async () => {revalidateTag("theme");},}),],});createTheme({ colorUsages: true })scans every registered collection and global when Colors Delete runs — drafts included, blocks included. Payload internals (payload-*) and Theme itself are skipped. A{ collections, globals }list still names slugs. Omit the option, or pass empty arrays, and every additional color is unused and deletes immediately. -
Seed the row on deploy. A migration
up()writes a published version so the site renders whatbison-theme.cssrendered before anyone opens the admin:import { seedFeatures, seedRoles, seedTheme } from "@bison-lab/payload-core";import bisonConfig from "../bison.config.json";export async function up({ payload }) {await seedTheme(payload, bisonConfig);await seedRoles(payload);await seedFeatures(payload);}Pass the same
extrasinto the seeds as intocreateRoles/createFeatures, if any. -
Serve the catalogue. A Next route around
serveFontfrom@bison-lab/fonts/server, listed inserverExternalPackages, at the path you pass asfontsBaseUrl(default/fonts). -
Render the published theme in the root layout. Import from
@bison-lab/payload-core/themeso the SEO plugin stays off that route.import { getPublishedIdentity, getPublishedTheme, themeHead } from "@bison-lab/payload-core/theme";import bisonConfig from "../bison.config.json";const theme = await getPublishedTheme(payload, bisonConfig);const identity = await getPublishedIdentity(payload, {lockup: { url: "/logo-lockup.svg", alt: "Acme" },mark: { url: "/logo-mark.svg", alt: "Acme mark" },});const { css, preloads } = themeHead(theme, { identity });Custom Colors need the Theme document:
themeHeadFromDoc(doc, bisonConfig, { identity })emits--coral-*and[data-look]solookFieldkeys paint. Empty logo, favicon, and mobile-menu mark use the fallback passed intocreateTheme.
Then payload generate:importmap (Theme fields resolve from @bison-lab/payload-core/admin), payload generate:types, and payload migrate:create.
lookField() and colorTokenField() live on the payload-blocks main entry and read this Global. See Entry points.
Rich Text editor fonts
Section titled “Rich Text editor fonts”Payload sets a Rich Text field’s editor in a serif (var(--font-serif)), so in the admin an editor’s prose reads in a different face from the published page and the live preview beside it. richTextEditorFonts() puts the editor, and the version diff of it, on the Theme body font:
import { richTextEditorFonts } from "@bison-lab/payload-core";
plugins: [richTextEditorFonts()],It is one admin stylesheet, mounted as a provider, that reads --font-body. That variable reaches the admin only when the admin layout renders the Theme head (themeHead), as described above. Headings in the editor follow whatever the admin sets on h1–h6. Run payload generate:importmap once after adding the plugin.