mdxui

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}
/>

Best Practices

Performance

  1. Limit columns - Show only essential fields in ListView
  2. Paginate - Use reasonable page sizes (25-50 items)
  3. Filter early - Apply filters before rendering

User Experience

  1. Default values - Pre-populate common fields in CreateView
  2. Field grouping - Use tabs/sections for complex forms
  3. Validation - Provide clear, actionable error messages
  4. Confirmations - Require confirmation for destructive actions

Security

  1. Access control - Use Payload's built-in access control
  2. Validation - Always validate on the server
  3. Hooks - Use beforeChange hooks for data sanitization

On this page