MCPInstall

payload-admin-components

Use when building or customizing Payload CMS Admin Panel components. Covers component types (Root, Collection, Global, Field), Server vs Client components, hooks, styling, performance, and troubleshooting.

Installs
61
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/timschoch/skilly --skill payload-admin-components
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads payload-admin-components automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

Custom Components in Payload CMS

Custom Components allow you to fully customize the Admin Panel by swapping in your own React components. You can replace nearly every part of the interface or add entirely new functionality.

Component Types

There are four main types of Custom Components:

  1. Root Components - Affect the Admin Panel globally (logo, nav, header)
  2. Collection Components - Specific to collection views
  3. Global Components - Specific to global document views
  4. Field Components - Custom field UI and cells

Defining Custom Components

Component Paths

Components are defined using file paths (not direct imports) to keep the config lightweight and Node.js compatible.

import { buildConfig } from 'payload'

export default buildConfig({
  admin: {
    components: {
      logout: {
        Button: '/src/components/Logout#MyComponent', // Named export
      },
      Nav: '/src/components/Nav', // Default export
    },
  },
})

Component Path Rules:

  1. Paths are relative to project root (or config.admin.importMap.baseDir)
  2. For named exports: append #ExportName or use exportName property
  3. For default exports: no suffix needed
  4. File extensions can be omitted

Component Config Object

Instead of a string path, you can pass a config object:

{
  logout: {
    Button: {
      path: '/src/components/Logout',
      exportName: 'MyComponent',
      clientProps: { customProp: 'value' },
      serverProps: { asyncData: someData },
    },
  },
}

Config Properties:

PropertyDescription
pathFile path to component (named exports via #)
exportNameNamed export (alternative to # in path)
clientPropsProps for Client Components (must be serializable)
serverPropsProps for Server Components (can be non-serializable)

Setting Base Directory

import path from 'path'
import { fileURLToPath } from 'node:url'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

export default buildConfig({
  admin: {
    importMap: {
      baseDir: path.resolve(dirname, 'src'), // Set base directory
    },
    components: {
      Nav: '/components/Nav', // Now relative to src/
    },
  },
})

Server vs Client Components

All components are React Server Components by default.

Server Components (Default)

Can use Local API directly, perform async operations, and access full Payload instance.

import React from 'react'
import type { Payload } from 'payload'

async function MyServerComponent({ payload }: { payload: Payload }) {
  const page = await payload.findByID({
    collection: 'pages',
    id: '123',
  })

  return <p>{page.title}</p>
}

export default MyServerComponent

Client Components

Use the 'use client' directive for interactivity, hooks, state, etc.

'use client'
import React, { useState } from 'react'

export function MyClientComponent() {
  const [count, setCount] = useState(0)

  return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
}

Important: Client Components cannot receive non-serializable props (functions, class instances, etc.). Payload automatically strips these when passing to client components.

Default Props

All Custom Components receive these props by default:

PropDescriptionType
payloadPayload instance (Local API access)Payload
i18nInternationalization objectI18n
localeCurrent locale (if localization enabled)string

Server Component Example:

async function MyComponent({ payload, i18n, locale }) {
  const data = await payload.find({
    collection: 'posts',
    locale,
  })

  return <div>{data.docs.length} posts</div>
}

Client Component Example:

'use client'
import { usePayload, useLocale, useTranslation } from '@payloadcms/ui'

export function MyComponent() {
  // Access via hooks in client components
  const { getLocal, getByID } = usePayload()
  const locale = useLocale()
  const { t, i18n } = useTranslation()

  return <div>{t('myKey')}</div>
}

Custom Props

Pass additional props using clientProps or serverProps:

{
  logout: {
    Button: {
      path: '/components/Logout',
      clientProps: {
        buttonText: 'Sign Out',
        onLogout: () => console.log('Logged out'),
      },
    },
  },
}

Receive in component:

'use client'
export function Logout({ buttonText, onLogout }) {
  return <button onClick={onLogout}>{buttonText}</button>
}

Root Components

Root Components affect the entire Admin Panel.

Available Root Components

ComponentDescriptionConfig Path
NavEntire navigation sidebaradmin.components.Nav
graphics.IconSmall icon (used in nav)admin.components.graphics.Icon
graphics.LogoFull logo (used on login)admin.components.graphics.Logo
logout.ButtonLogout buttonadmin.components.logout.Button
actionsHeader actions (array)admin.components.actions
headerAbove header (array)admin.components.header
beforeDashboardBefore dashboard content (array)admin.components.beforeDashboard
afterDashboardAfter dashboard content (array)admin.components.afterDashboard
beforeLoginBefore login form (array)admin.components.beforeLogin
afterLoginAfter login form (array)admin.components.afterLogin
beforeNavLinksBefore nav links (array)admin.components.beforeNavLinks
afterNavLinksAfter nav links (array)admin.components.afterNavLinks
settingsMenuSettings menu items (array)admin.components.settingsMenu
providersCustom React Context providersadmin.components.providers
viewsCustom views (dashboard, etc.)admin.components.views

Example: Custom Logo

export default buildConfig({
  admin: {
    components: {
      graphics: {
        Logo: '/components/Logo',
        Icon: '/components/Icon',
      },
    },
  },
})
// components/Logo.tsx
export default function Logo() {
  return <img src="/logo.png" alt="My Brand" width={200} />
}

Example: Header Actions

export default buildConfig({
  admin: {
    components: {
      actions: ['/components/ClearCacheButton', '/components/PreviewButton'],
    },
  },
})
// components/ClearCacheButton.tsx
'use client'
export default function ClearCacheButton() {
  return (
    <button
      onClick={async () => {
        await fetch('/api/clear-cache', { method: 'POST' })
        alert('Cache cleared!')
      }}
    >
      Clear Cache
    </button>
  )
}

Collection Components

Collection Components are specific to a collection's views.

import type { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    components: {
      // Edit view components
      edit: {
        PreviewButton: '/components/PostPreview',
        SaveButton: '/components/CustomSave',
        SaveDraftButton: '/components/CustomSaveDraft',
        PublishButton: '/components/CustomPublish',
      },

      // List view components
      list: {
        Header: '/components/PostsListHeader',
        beforeList: ['/components/ListFilters'],
        afterList: ['/components/ListFooter'],
      },
    },
  },
  fields: [
    // ...
  ],
}

Global Components

Similar to Collection Components but for Global documents.

import type { GlobalConfig } from 'payload'

export const Settings: GlobalConfig = {
  slug: 'settings',
  admin: {
    components: {
      edit: {
        PreviewButton: '/components/SettingsPreview',
        SaveButton: '/components/SettingsSave',
      },
    },
  },
  fields: [
    // ...
  ],
}

Field Components

Customize how fields render in Edit and List views.

Field Component (Edit View)

{
  name: 'status',
  type: 'select',
  options: ['draft', 'published'],
  admin: {
    components: {
      Field: '/components/StatusField',
    },
  },
}
// components/StatusField.tsx
'use client'
import { useField } from '@payloadcms/ui'
import type { SelectFieldClientComponent } from 'payload'

export const StatusField: SelectFieldClientComponent = ({ path, field }) => {
  const { value, setValue } = useField({ path })

  return (
    <div>
      <label>{field.label}</label>
      <select value={value} onChange={(e) => setValue(e.target.value)}>
        {field.options.map((option) => (
          <option key={option.value} value={option.value}>
            {option.label}
          </option>
        ))}
      </select>
    </div>
  )
}

Cell Component (List View)

{
  name: 'status',
  type: 'select',
  options: ['draft', 'published'],
  admin: {
    components: {
      Cell: '/components/StatusCell',
    },
  },
}
// components/StatusCell.tsx
import type { SelectFieldCellComponent } from 'payload'

export const StatusCell: SelectFieldCellComponent = ({ data, cellData }) => {
  const isPublished = cellData === 'published'

  return (
    <span
      style={{
        color: isPublished ? 'green' : 'orange',
        fontWeight: 'bold',
      }}
    >
      {cellData}
    </span>
  )
}

UI Field (Presentational Only)

Special field type for adding custom UI without affecting data:

{
  name: 'refundButton',
  type: 'ui',
  admin: {
    components: {
      Field: '/components/RefundButton',
    },
  },
}
// components/RefundButton.tsx
'use client'
import { useDocumentInfo } from '@payloadcms/ui'

export default function RefundButton() {
  const { id } = useDocumentInfo()

  return (
    <button
      onClick={async () => {
        await fetch(`/api/orders/${id}/refund`, { method: 'POST' })
        alert('Refund processed')
      }}
    >
      Process Refund
    </button>
  )
}

Using Hooks

Payload provides many React hooks for Client Components:

'use client'
import {
  useAuth, // Current user
  useConfig, // Payload config (client-safe)
  useDocumentInfo, // Current document info (id, slug, etc.)
  useField, // Field value and setValue
  useForm, // Form state and dispatch
  useFormFields, // Multiple field values (optimized)
  useLocale, // Current locale
  useTranslation, // i18n translations
  usePayload, // Local API methods
} from '@payloadcms/ui'

export function MyComponent() {
  const { user } = useAuth()
  const { config } = useConfig()
  const { id, collection } = useDocumentInfo()
  const locale = useLocale()
  const { t } = useTranslation()

  return <div>Hello {user?.email}</div>
}

Important: These hooks only work in Client Components within the Admin Panel context.

Accessing Payload Config

In Server Components:

async function MyServerComponent({ payload }) {
  const { config } = payload
  return <div>{config.serverURL}</div>
}

In Client Components:

'use client'
import { useConfig } from '@payloadcms/ui'

export function MyClientComponent() {
  const { config } = useConfig() // Client-safe config
  return <div>{config.serverURL}</div>
}

Important: Client Components receive a serializable version of the config (functions, validation, etc. are stripped).

Field Config Access

Server Component:

import type { TextFieldServerComponent } from 'payload'

export const MyFieldComponent: TextFieldServerComponent = ({ field }) => {
  return <div>Field name: {field.name}</div>
}

Client Component:

'use client'
import type { TextFieldClientComponent } from 'payload'

export const MyFieldComponent: TextFieldClientComponent = ({ clientField }) => {
  // clientField has non-serializable props removed
  return <div>Field name: {clientField.name}</div>
}

Translations (i18n)

Server Component:

import { getTranslation } from '@payloadcms/translations'

async function MyServerComponent({ i18n }) {
  const translatedTitle = getTranslation(myTranslation, i18n)
  return <p>{translatedTitle}</p>
}

Client Component:

'use client'
import { useTranslation } from '@payloadcms/ui'

export function MyClientComponent() {
  const { t, i18n } = useTranslation()

  return (
    <div>
      <p>{t('namespace:key', { variable: 'value' })}</p>
      <p>Language: {i18n.language}</p>
    </div>
  )
}

Styling Components

Using CSS Variables

import './styles.scss'

export function MyComponent() {
  return <div className="my-component">Custom Component</div>
}
// styles.scss
.my-component {
  background-color: var(--theme-elevation-500);
  color: var(--theme-text);
  padding: var(--base);
  border-radius: var(--border-radius-m);
}

Importing Payload SCSS

@import '~@payloadcms/ui/scss';

.my-component {
  @include mid-break {
    background-color: var(--theme-elevation-900);
  }
}

Common Patterns

Conditional Field Visibility

'use client'
import { useFormFields } from '@payloadcms/ui'
import type { TextFieldClientComponent } from 'payload'

export const ConditionalField: TextFieldClientComponent = ({ path }) => {
  const showField = useFormFields(([fields]) => fields.enableFeature?.value)

  if (!showField) return null

  return <input type="text" />
}

Loading Data from API

'use client'
import { useState, useEffect } from 'react'

export function DataLoader() {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch('/api/custom-data')
      .then((res) => res.json())
      .then(setData)
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

Using Local API in Server Components

import type { Payload } from 'payload'

async function RelatedPosts({ payload, id }: { payload: Payload; id: string }) {
  const post = await payload.findByID({
    collection: 'posts',
    id,
    depth: 0,
  })

  const related = await payload.find({
    collection: 'posts',
    where: {
      category: { equals: post.category },
      id: { not_equals: id },
    },
    limit: 5,
  })

  return (
    <div>
      <h3>Related Posts</h3>
      <ul>
        {related.docs.map((doc) => (
          <li key={doc.id}>{doc.title}</li>
        ))}
      </ul>
    </div>
  )
}

export default RelatedPosts

Performance Best Practices

1. Minimize Client Bundle Size

// In Admin Panel UI: import from @payloadcms/ui
'use client'
import { Button } from '@payloadcms/ui'

// In frontend code: use specific paths for tree-shaking
import { Button } from '@payloadcms/ui/elements/Button'

2. Optimize Re-renders

// Bad: Re-renders on every form change
'use client'
import { useForm } from '@payloadcms/ui'

export function MyComponent() {
  const { fields } = useForm()
  // Re-renders on ANY field change
}

// Good: Only re-renders when specific field changes
;('use client')
import { useFormFields } from '@payloadcms/ui'

export function MyComponent({ path }) {
  const value = useFormFields(([fields]) => fields[path])
  // Only re-renders when this field changes
}

3. Use Server Components When Possible

Only use Client Components when you need:

  • State (useState, useReducer)
  • Effects (useEffect)
  • Event handlers (onClick, onChange)
  • Browser APIs (localStorage, window)

4. React Best Practices

  • Use React.memo() for expensive components
  • Implement proper key props in lists
  • Avoid inline function definitions in renders
  • Use Suspense boundaries for async operations

Import Map

Payload generates an import map at app/(payload)/admin/importMap.js that resolves all component paths.

Regenerate manually:

payload generate:importmap

Override location:

export default buildConfig({
  admin: {
    importMap: {
      baseDir: path.resolve(dirname, 'src'),
      importMapFile: path.resolve(dirname, 'app', 'custom-import-map.js'),
    },
  },
})

Type Safety

Use Payload's TypeScript types for components:

import type {
  TextFieldServerComponent,
  TextFieldClientComponent,
  TextFieldCellComponent,
} from 'payload'

export const MyFieldComponent: TextFieldServerComponent = (props) => {
  // Fully typed props
}

Troubleshooting

"useConfig is undefined" or similar hook errors

Cause: Dependency version mismatch between Payload packages.

Solution: Pin all @payloadcms/* packages to the exact same version:

{
  "dependencies": {
    "payload": "3.0.0",
    "@payloadcms/ui": "3.0.0",
    "@payloadcms/richtext-lexical": "3.0.0"
  }
}

Component not loading

  1. Check file path is correct (relative to baseDir)
  2. Verify named export syntax: /path/to/file#ExportName
  3. Run payload generate:importmap to regenerate
  4. Check for TypeScript errors in component file

Resources

FAQ

What does the payload-admin-components skill do?

Use when building or customizing Payload CMS Admin Panel components. Covers component types (Root, Collection, Global, Field), Server vs Client components, hooks, styling, performance, and troubleshooting.

How do I install payload-admin-components?

Run npx skills add https://github.com/timschoch/skilly --skill payload-admin-components in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/payload-admin-components/ (personal) or .claude/skills/payload-admin-components/ (project).

Which AI agents can use payload-admin-components?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is payload-admin-components safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains payload-admin-components?

It is published by timschoch in timschoch/skilly. It has 61 recorded installs.

More from timschoch/skilly

View all →

cleanup-merged-branches

timschoch/skilly

Delete remote branches whose PR was merged. Read-only dry-run, then confirm, then delete.

git-shortcuts

timschoch/skilly

Translates shorthand git/GitHub workflow commands into the correct sequence of actions. ALWAYS trigger when the user's message contains a "--" prefixed shorthand like: "--c", "--p", "--cp", "--pr", "--prm", "--cppr", "--cpprm", "--ff", "--release", "--pub", "--close" — with or without a trailing "?". The "--" prefix is required — do NOT trigger on bare letters like "c" or "pr" appearing in normal sentences. NEVER invoke this skill autonomously — it MUST always be user-invoked via an explicit "--" shorthand in their message.

step-by-step

timschoch/skilly

Walk the user through a manual procedure step by step, with every value spelled out and every link clickable.

667 installs

plan-critics

timschoch/skilly

Critiques an implementation plan — or a wayfinder map/ticket — from multiple angles (security, simplicity, edge cases, scope creep, codebase conflicts, tech-specific best practices) via parallel critic subagents. Trigger: user says "kritisiere den plan", "plan check", "plan-kritik", "blinde flecken", "/plan-critics" — also invoke when a planning agent wants to validate its own plan before presenting it, or a wayfinder session wants a resolution checked before closing a ticket. Must be called at top-level (not inside a subagent) — it uses the Task tool.

qa

timschoch/skilly

Interactive QA session where user reports bugs or issues conversationally, and the agent files GitHub issues. Explores the codebase in the background for context and domain language. Use when user wants to report bugs, do QA, file issues conversationally, or mentions "QA session".

Testing & QA664 installs

1-3-1

timschoch/skilly

Frame a problem or decision as 1-3-1: one problem, three distinct solutions, one recommendation. Use when the user brings a non-trivial decision or fork and wants options weighed before a pick ("what should I do about X", "help me decide", "weigh the tradeoffs"), or names "1-3-1" or "do a 131". Skip questions with one right answer, just answer those.

662 installs

Popular frontend & design skills

View category →

setup-matt-pocock-skills

mattpocock/skills

Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.

Frontend & Design1.1M installs

frontend-design

anthropics/skills

Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.

OfficialFrontend & Design1M installs

hyperframes-animation

heygen-com/hyperframes

All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.

prototype

mattpocock/skills

Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.

Frontend & Design999.2K installs

hyperframes-core

heygen-com/hyperframes

The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.

Frontend & Design930.5K installs

vercel-react-best-practices

vercel-labs/agent-skills

React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.

OfficialFrontend & Design843.1K installs