Admin Customization
Custom Payload CMS admin views with mdxui components
Admin Customization
The Admin Override components allow you to replace Payload CMS's default admin UI with custom views styled using mdxui primitives. These components maintain full Payload functionality while integrating seamlessly with your design system.
Overview
mdxui provides three admin override components:
- ListView - Custom collection list view with sorting, filtering, and pagination
- EditView - Custom document edit view with flexible field layouts
- CreateView - Custom document creation view with default values
These components are part of the AppComponents interface and can be configured per-collection or globally.
Quick Start
import type { AppComponents } from 'mdxui'
import { ListView, EditView, CreateView } from '@mdxui/cockpit'
export const components: AppComponents = {
// Required layout components
App,
Shell,
Sidebar,
Header,
Dashboard,
Settings,
// Optional Payload admin overrides
ListView,
EditView,
CreateView,
}ListView
Custom collection list view with advanced table features.
Type Signature
interface ListViewProps {
/** Collection slug */
collection: string
/** Columns to display */
columns?: string[]
/** Filter configuration */
filters?: Array<{
field: string
label: string
type: 'text' | 'select' | 'date' | 'boolean'
options?: Array<{ label: string; value: string }>
}>
/** Default sort field */
defaultSort?: string
/** Items per page */
pageSize?: number
}Basic Usage
import { ListView } from '@mdxui/cockpit'
// Default configuration
<ListView collection="posts" />
// Custom columns
<ListView
collection="posts"
columns={['title', 'author', 'status', 'createdAt']}
/>
// With sorting and pagination
<ListView
collection="apiKeys"
columns={['name', 'key', 'project', 'lastUsed']}
defaultSort="createdAt"
pageSize={50}
/>Filtering
Add custom filters to the list view:
<ListView
collection="posts"
columns={['title', 'author', 'status', 'publishedAt']}
filters={[
{
field: 'status',
label: 'Status',
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
{ label: 'Archived', value: 'archived' }
]
},
{
field: 'author',
label: 'Author',
type: 'select'
},
{
field: 'publishedAt',
label: 'Published Date',
type: 'date'
}
]}
/>Sorting
Enable column-based sorting:
<ListView
collection="users"
columns={['name', 'email', 'role', 'lastLogin']}
defaultSort="createdAt"
/>Users can click column headers to:
- Sort ascending (first click)
- Sort descending (second click)
- Remove sort (third click)
Pagination
Configure pagination behavior:
// Show 10 items per page
<ListView
collection="webhooks"
pageSize={10}
/>
// Large lists with 100 items per page
<ListView
collection="logs"
pageSize={100}
/>The ListView automatically displays:
- Page count and current position
- Previous/Next navigation
- Jump to specific page
- Items per page selector
Collection-Specific Configuration
Configure ListView per collection in Payload config:
import { Collections } from 'payload/generated-types'
import { ListView } from '@mdxui/cockpit'
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
components: {
views: {
List: {
Component: () => (
<ListView
collection="posts"
columns={['title', 'author', 'status', 'publishedAt']}
filters={[
{ field: 'status', label: 'Status', type: 'select' }
]}
defaultSort="publishedAt"
pageSize={25}
/>
)
}
}
}
}
}EditView
Custom edit view for modifying existing documents.
Type Signature
interface EditViewProps {
/** Collection slug */
collection: string
/** Document ID */
id: string
/** Fields to show */
fields?: string[]
/** Field layout mode */
layout?: 'default' | 'tabs' | 'sections'
}Basic Usage
import { EditView } from '@mdxui/cockpit'
// Default layout
<EditView collection="posts" id="post-123" />
// Show specific fields only
<EditView
collection="posts"
id="post-123"
fields={['title', 'content', 'status']}
/>
// Use tabs layout
<EditView
collection="posts"
id="post-123"
layout="tabs"
/>Layout Modes
Default Layout
Single column form layout:
<EditView
collection="posts"
id="post-123"
layout="default"
/>Fields render vertically in a single column.
Tabs Layout
Organize fields into tabbed sections:
<EditView
collection="posts"
id="post-123"
layout="tabs"
/>Configure tabs in collection config:
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
type: 'tabs',
tabs: [
{
label: 'Content',
fields: [
{ name: 'title', type: 'text' },
{ name: 'content', type: 'richText' }
]
},
{
label: 'Meta',
fields: [
{ name: 'slug', type: 'text' },
{ name: 'excerpt', type: 'textarea' }
]
},
{
label: 'Settings',
fields: [
{ name: 'status', type: 'select' },
{ name: 'publishedAt', type: 'date' }
]
}
]
}
]
}Sections Layout
Group fields into collapsible sections:
<EditView
collection="posts"
id="post-123"
layout="sections"
/>Configure sections in collection config:
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
type: 'collapsible',
label: 'Content',
fields: [
{ name: 'title', type: 'text' },
{ name: 'content', type: 'richText' }
]
},
{
type: 'collapsible',
label: 'SEO',
fields: [
{ name: 'metaTitle', type: 'text' },
{ name: 'metaDescription', type: 'textarea' }
]
}
]
}Field Filtering
Show only specific fields:
// Edit only title and status
<EditView
collection="posts"
id="post-123"
fields={['title', 'status']}
/>
// Quick publish view
<EditView
collection="posts"
id="post-123"
fields={['status', 'publishedAt']}
layout="default"
/>Custom Actions
The EditView includes built-in actions:
- Save Draft - Save without publishing
- Publish - Publish the document
- Delete - Remove the document (with confirmation)
Override actions in collection config:
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
components: {
views: {
Edit: {
Component: ({ id }) => {
const handleCustomAction = async () => {
// Custom logic
await customPublishWorkflow(id)
}
return (
<EditView
collection="posts"
id={id}
layout="tabs"
/>
)
}
}
}
}
}
}CreateView
Custom view for creating new documents.
Type Signature
interface CreateViewProps {
/** Collection slug */
collection: string
/** Initial field values */
defaultValues?: Record<string, any>
/** Fields to show */
fields?: string[]
}Basic Usage
import { CreateView } from '@mdxui/cockpit'
// Default create view
<CreateView collection="posts" />
// With default values
<CreateView
collection="posts"
defaultValues={{
status: 'draft',
author: currentUser.id
}}
/>
// Show specific fields only
<CreateView
collection="posts"
fields={['title', 'content', 'status']}
/>Default Values
Pre-populate fields when creating documents:
// Set default status
<CreateView
collection="posts"
defaultValues={{ status: 'draft' }}
/>
// Set multiple defaults
<CreateView
collection="apiKeys"
defaultValues={{
project: currentProject.id,
scopes: ['read'],
expiresAt: addDays(new Date(), 30)
}}
/>Field Selection
Show only required fields for quick creation:
// Minimal post creation
<CreateView
collection="posts"
fields={['title', 'content']}
defaultValues={{
status: 'draft',
author: currentUser.id,
publishedAt: new Date()
}}
/>Collection-Specific Configuration
Configure CreateView per collection:
export const APIKeys: CollectionConfig = {
slug: 'apiKeys',
admin: {
components: {
views: {
Edit: {
Component: () => (
<CreateView
collection="apiKeys"
fields={['name', 'scopes', 'expiresAt']}
defaultValues={{
project: getCurrentProject(),
enabled: true
}}
/>
)
}
}
}
}
}Custom Validation
Implement custom validation in collection config:
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
required: true,
validate: (value) => {
if (!value || value.length < 5) {
return 'Title must be at least 5 characters'
}
return true
}
},
{
name: 'slug',
type: 'text',
required: true,
validate: async (value, { operation, data }) => {
// Only validate on create or if slug changed
if (operation === 'create' || data.slug !== value) {
const exists = await payload.find({
collection: 'posts',
where: { slug: { equals: value } }
})
if (exists.docs.length > 0) {
return 'Slug must be unique'
}
}
return true
}
}
]
}Field-Level Validation
Add validation to specific fields:
{
name: 'email',
type: 'email',
required: true,
validate: (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) {
return 'Invalid email format'
}
return true
}
}Conditional Validation
Validate based on other field values:
{
name: 'expiresAt',
type: 'date',
validate: (value, { data }) => {
if (data.neverExpires) {
return true // Skip validation if never expires
}
if (!value) {
return 'Expiration date required'
}
if (new Date(value) < new Date()) {
return 'Expiration date must be in the future'
}
return true
}
}Hooks Integration
Use Payload hooks with custom admin views:
Before Change Hook
Modify data before saving:
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
beforeChange: [
({ data, operation }) => {
// Auto-generate slug from title
if (operation === 'create' && !data.slug) {
data.slug = slugify(data.title)
}
// Set published timestamp
if (data.status === 'published' && !data.publishedAt) {
data.publishedAt = new Date()
}
return data
}
]
}
}After Change Hook
Perform actions after saving:
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
afterChange: [
async ({ doc, operation, req }) => {
// Send notification on publish
if (operation === 'update' && doc.status === 'published') {
await sendPublishNotification(doc.id, req.user)
}
// Invalidate cache
await revalidatePath(`/blog/${doc.slug}`)
return doc
}
]
}
}Before Delete Hook
Prevent deletion or clean up:
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
beforeDelete: [
async ({ id, req }) => {
// Prevent deleting published posts
const post = await req.payload.findByID({
collection: 'posts',
id
})
if (post.status === 'published') {
throw new Error('Cannot delete published posts')
}
// Clean up related data
await req.payload.delete({
collection: 'comments',
where: { post: { equals: id } }
})
}
]
}
}Access Control Hook
Control field-level access:
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'internalNotes',
type: 'textarea',
access: {
read: ({ req }) => {
// Only admins can read internal notes
return req.user?.role === 'admin'
},
update: ({ req }) => {
return req.user?.role === 'admin'
}
}
}
]
}Global Configuration
Apply admin overrides globally in Payload config:
import { buildConfig } from 'payload/config'
import { ListView, EditView, CreateView } from '@mdxui/cockpit'
export default buildConfig({
admin: {
components: {
views: {
List: ListView,
Edit: EditView
}
}
},
collections: [
// Collections inherit global admin components
Posts,
Pages,
APIKeys
]
})Per-Collection Override
Override admin views for specific collections:
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
components: {
views: {
// Custom list view for posts only
List: {
Component: () => (
<ListView
collection="posts"
columns={['title', 'author', 'status']}
filters={[
{ field: 'status', label: 'Status', type: 'select' }
]}
pageSize={25}
/>
)
},
// Custom edit view with tabs
Edit: {
Component: ({ id }) => (
<EditView
collection="posts"
id={id}
layout="tabs"
/>
)
}
}
}
}
}Styling
Admin views use mdxui primitives and inherit your theme:
// All views use primitives from @mdxui/primitives
import { Button } from '@mdxui/primitives/button'
import { Card } from '@mdxui/primitives/card'
import { Table } from '@mdxui/primitives/table'
import { Input } from '@mdxui/primitives/input'Customize with Tailwind classes or theme variables:
// In your tailwind.config.js
export default {
theme: {
extend: {
colors: {
// Custom admin theme colors
'admin-primary': '#3b82f6',
'admin-secondary': '#8b5cf6'
}
}
}
}Responsive Behavior
All admin views are fully responsive:
Desktop (≥1024px)
┌─────────────────────────────────────────────┐
│ Posts [Create] │
├─────────────────────────────────────────────┤
│ [Search...] [Filter ▼] [25 per page ▼] │
│ ┌────────────────────────────────────────┐ │
│ │ Title │ Author │ Status │ Date │ │
│ ├────────────────────────────────────────┤ │
│ │ Post 1 │ John │ Draft │ Dec 19 │ │
│ │ Post 2 │ Jane │ Pub │ Dec 18 │ │
│ └────────────────────────────────────────┘ │
│ « 1 2 3 ... 10 » │
└─────────────────────────────────────────────┘Mobile (under 768px)
┌──────────────────────────┐
│ Posts [+] │
├──────────────────────────┤
│ [Search...] │
│ ┌────────────────────┐ │
│ │ Post 1 │ │
│ │ John - Dec 19 │ │
│ │ [Draft] │ │
│ └────────────────────┘ │
│ « 1 2 3 » │
└──────────────────────────┘Accessibility
Admin views implement WCAG 2.1 AA standards:
- Keyboard navigation - Full keyboard support
- Screen readers - ARIA labels and roles
- Focus management - Logical tab order
- Color contrast - Meets WCAG contrast ratios
- Form validation - Clear error messages
// Example accessible form field
<FormField
name="title"
label="Post Title"
required
aria-describedby="title-help"
aria-invalid={!!errors.title}
error={errors.title?.message}
/>Related Components
- Dashboard - Main dashboard view
- Settings - Settings page layout
- API Keys - API key management
- Webhooks - Webhook configuration
Best Practices
Performance
- Limit columns - Show only essential fields in ListView
- Paginate - Use reasonable page sizes (25-50 items)
- Filter early - Apply filters before rendering
User Experience
- Default values - Pre-populate common fields in CreateView
- Field grouping - Use tabs/sections for complex forms
- Validation - Provide clear, actionable error messages
- Confirmations - Require confirmation for destructive actions
Security
- Access control - Use Payload's built-in access control
- Validation - Always validate on the server
- Hooks - Use beforeChange hooks for data sanitization