Admin Theming and Branding
How the KDIGI CMS admin UI is styled, and how to change its colors, logo, name and labels.
The admin is Payload's admin panel with custom components and one stylesheet. Payload still owns authentication, permissions, site (tenant) selection, queries and the document lifecycle; the theme only changes presentation. Branding here applies to the admin only. Each site's own brand (logo, favicon, primary color, theme) is site data on the Sites collection (Thương hiệu tab, branding group) and is not affected.
Where things live
Section titled “Where things live”| Concern | File |
|---|---|
| Theme tokens and all admin CSS overrides | src/app/(payload)/custom.css |
Tailwind v4 entry, scoped to .twp, and the shadcn token bridge | src/app/(payload)/tailwind-admin.css |
| Loads the CSS and the Inter font (registered as an admin provider) | src/components/admin/css-injector.tsx |
Logo and icon components (admin.components.graphics) | src/components/admin/logo.tsx, src/components/admin/icon.tsx |
| Brand images | public/brand/ (kdigi-logo.svg, kdigi-logo-white.svg, kdigi-mark.svg) |
| Browser tab title, favicon, description | admin.meta in src/payload.config.ts |
| Login screen branding | src/components/admin/login-workspace-panel.tsx |
| Sidebar, dashboard, list and edit views | admin-nav*.tsx, editorial-dashboard-view.tsx, premium-*.tsx in src/components/admin/ |
| shadcn/ui primitives | src/components/ui/ (configured by components.json) |
| Markdown editor theme | src/components/admin/milkdown-theme-tokens.css and sibling milkdown-*.css |
The stylesheet is imported once by CSSInjector, registered in admin.components.providers. There is no second theme provider; light/dark follows Payload's own theme switch (admin.theme: 'all', stored per user) through the data-theme attribute on <html>.
Design tokens
Section titled “Design tokens”Everything is driven by CSS custom properties defined in custom.css.
Color tokens. Payload's native --theme-* variables, defined once for :root, [data-theme="light"] and once for [data-theme="dark"]. The default palette is neutral (grayscale OKLCH); the brand accent is a token you can change.
| Token | Role |
|---|---|
--theme-bg, --theme-input-bg, --theme-text, --theme-muted-text | Page, input, text, secondary text |
--theme-border-color, --harbor-control-border | Decorative borders, control borders |
--theme-ring | Focus ring |
--theme-elevation-0 ... --theme-elevation-1000 | Payload's surface scale |
--theme-brand, --theme-brand-hover, --theme-brand-active | Accent: primary buttons, links, active nav, checked controls, selected rows |
--theme-brand-subtle, --theme-brand-muted | Soft accent backgrounds and borders |
--harbor-on-primary | Text color on top of --theme-brand |
--theme-nav-bg, --theme-nav-border | Sidebar |
Success, warning and error colors keep their own semantic values; do not replace them with the accent.
Layout and type tokens (:root, prefixed --kdigi-):
| Token group | Examples |
|---|---|
| Spacing | --kdigi-space-1 ... --kdigi-space-8 (4/8/12/16/24/32 px) |
| Control height | --kdigi-control-height (36 px; 44 px on phones), --kdigi-control-compact-height (32 px) |
| Type scale | --kdigi-font-size-meta/body/section/page (12/14/16/24 px), matching --kdigi-line-* and --kdigi-weight-body/label/heading |
| Navigation | --kdigi-nav-width (256 px), --kdigi-nav-compact-width (48 px) |
| Motion | --kdigi-duration (160 ms); prefers-reduced-motion shortens animations |
| Other | --font-body (Inter Variable, then system fonts), --style-radius-s/m/l (6/8/10 px), --app-header-height |
html is 16 px and body text 14 px. --base-body-size: 16 must match html; lowering it produces fractional spacing in Payload's base() calculations.
The .twp rule (Tailwind and shadcn)
Section titled “The .twp rule (Tailwind and shadcn)”tailwind-admin.css loads only Tailwind's theme and utilities layers (@layer theme, base, components, utilities). No global preflight is installed, so Payload's own styles are never reset. A minimal reset lives inside @layer base, scoped to .twp descendants.
Rules for custom React components:
- Put Tailwind utility classes and shadcn components inside a wrapper element with class
twp:<div className="twp"><Button variant="outline">Action</Button></div>. .twpmaps the shadcn variables (--background,--primary,--border,--ring,--sidebar-*, ...) onto the--theme-*tokens, so shadcn components follow the admin palette in light and dark mode. Do not declare an independent palette.- Portaled content (popovers, tooltips, dialogs rendered outside the wrapper) must carry its own
twpclass on the content root, not only on the trigger. dark:utilities follow[data-theme="dark"](custom variant intailwind-admin.css).- Do not put utilities on Payload's native elements; utilities are generated globally, and
.twponly scopes the CSS variables and the reset.
export const MyPanel = () => ( <div className="twp rounded-lg border border-border bg-card p-4"> <Button variant="outline">Action</Button> </div>)components.json points the shadcn CLI at src/app/(payload)/tailwind-admin.css, so components added with the CLI land in src/components/ui/ with the @/ aliases.
Rebranding
Section titled “Rebranding”Colors
Section titled “Colors”Edit src/app/(payload)/custom.css:
- Change the
--theme-brand,--theme-brand-hover,--theme-brand-active,--theme-brand-subtleand--theme-brand-mutedvalues in both the light block (:root, [data-theme="light"]) and the dark block ([data-theme="dark"]). Adjust--harbor-on-primaryso button text stays readable (and--theme-ringif you want a tinted focus ring). - Optionally tint the surfaces (
--theme-elevation-*,--theme-nav-bg). - Nothing else is required: the
.twpbridge, the sidebar, buttons, links and the Markdown editor (milkdown-theme-tokens.css) all derive from these tokens.
Example (illustrative values):
:root, [data-theme="light"] { --theme-brand: oklch(0.55 0.2 260); --theme-brand-hover: oklch(0.5 0.2 260); --theme-brand-active: oklch(0.45 0.2 260); --theme-brand-subtle: oklch(0.96 0.03 260); --theme-brand-muted: oklch(0.9 0.06 260); --harbor-on-primary: oklch(0.99 0 0);}Check light and dark mode in /admin after the change, including a table row selection, a primary button and the editor.
Logo and favicon
Section titled “Logo and favicon”- Replace the files in
public/brand/with your own assets, keeping the file names:kdigi-logo.svg(wordmark for light mode),kdigi-logo-white.svg(wordmark for dark mode) andkdigi-mark.svg(square mark: compact sidebar, header, login, favicon). - If you use different names or formats, update the
src(andwidth/height) inlogo.tsxandicon.tsx, and the twoiconsentries inadmin.metainpayload.config.ts(these must be absolute URLs built fromserverURL). - Display sizes come from
custom.css:.kdigi-logois 128 px wide,.kdigi-iconis 28 px (24 px and 20 px in the header).Logorenders both wordmarks and CSS shows the one matchingdata-theme.
Product name and copy
Section titled “Product name and copy”Strings to change when renaming the product:
| String | Location |
|---|---|
| Tab title, title suffix, description, Open Graph text | admin.meta in src/payload.config.ts |
Header home label (CSS content) | .app-header__step-nav .step-nav__home::after in custom.css |
| Login brand name and tagline | login-workspace-panel.tsx |
Sidebar logo link aria-label | admin-nav-client.tsx |
| Dashboard welcome line | editorial-dashboard-view.tsx |
alt text | logo.tsx, icon.tsx |
The admin has no Open Graph image (defaultOGImageType: 'off'); keep it that way, because Payload's dynamic OG route cannot render the relative image URL the sidebar icon uses.
Admin labels and languages
Section titled “Admin labels and languages”The admin UI is bilingual, Vietnamese (default, fallbackLanguage: 'vi') and English (i18n.supportedLanguages in payload.config.ts). The two choices are independent: the header Content language switch changes ?locale=vi|en for content, the account menu Interface language changes the UI.
| What | Where |
|---|---|
| Collection names, field labels, descriptions | labels, label and admin.description in src/collections/*.ts, usually { vi, en } objects |
| Sidebar groups | admin.group in each collection |
| Automatic Vietnamese translation of English field labels/descriptions | src/lib/admin-display-labels.ts (lookup tables; display-only, never touches field names, enum values or content). Applied to all collections by src/lib/apply-premium-admin-presentation.ts |
| Custom views and widgets | Inline isEnglish ? '...' : '...' pairs in the component |
| Editor UI strings | src/components/admin/milkdown-ui-copy.ts |
| Payload core labels that need overriding | i18n.translations in payload.config.ts (for example the content/interface language labels) |
To add a language, add its Payload translation bundle to i18n.supportedLanguages and extend the { vi, en } label objects and the isEnglish branches you care about.
Custom admin views
Section titled “Custom admin views”apply-premium-admin-presentation.ts only changes admin metadata, labels and component registrations; names, types, access and hooks are untouched. It swaps native text, textarea, select and checkbox inputs for the premium-* field components unless a field already has a custom Field or is hidden. Other custom surfaces:
| Surface | Component |
|---|---|
| Sidebar | admin-nav.tsx (server, permission-filtered) and admin-nav-client.tsx; collapse state in localStorage key kdigi_cms_sidebar_collapsed |
| Dashboard | editorial-dashboard-view.tsx with dashboard-widgets.tsx and seo-audit-widget.tsx; shows live counts only |
| Content calendar | content-calendar/content-calendar-view.tsx, mounted at /admin/content-calendar |
| Collection lists | premium-collection-list-view.tsx, premium-collection-table.tsx |
| Edit view | premium-edit-view.tsx, a wrapper around Payload's default edit view (one form, one save lifecycle) |
| Media Library | media-grid-list-view.tsx (see media-storage.md) |
| Markdown editor | markdown-editor-field.tsx, milkdown-editor-core.tsx (see content-markdown.md) |
Markdown editor styling
Section titled “Markdown editor styling”The editor core imports @milkdown/crepe/theme/common/style.css, @milkdown/crepe/theme/frame.css, then milkdown-lark-theme.css, milkdown-layer-tokens.css and milkdown-editor-core.css. Its colors come from --mlk-* variables defined in milkdown-theme-tokens.css in terms of the admin --theme-* tokens, so rebranding the accent also rebrands the editor. Layer (z-index) values live in milkdown-layer-tokens.css; do not raise the whole editor wrapper above modals.
- Spacing, control heights and type sizes have a single owner each (
--kdigi-*tokens). Prefer changing the token over adding!importantoverrides, and check computed styles when editing the cascade. - Small screens: the sidebar becomes an overlay below 768 px, settings tabs go horizontal below 1024 px, controls grow to 44 px on phones.
- Payload's
@payloadcms/uiis patched (patches/@payloadcms__ui@3.90.2.patch, declared underpnpm.patchedDependenciesinpackage.json) so drawer toggleraria-labels resolve bilingual labels. Re-check the patch whenever you upgrade Payload; see upgrading.md. - Previewing the admin over plain HTTP from a non-loopback host (LAN IP, hostname) cannot log in, because browsers omit
Originand Payload then ignores the auth cookie. Uselocalhost, or HTTPS, and setPAYLOAD_PUBLIC_SERVER_URLto the exact origin you open (see configuration.md).
See also: architecture.md, roles-and-permissions.md.