Custom Payload CMS Fields
Payload provides 20+ built-in field types. For non-standard cases — custom React components for the admin panel while maintaining full server-side validation and storage logic.
Built-in Fields with Advanced Configuration
Field with conditional visibility:
{
name: 'discountPrice',
type: 'number',
admin: {
condition: (data, siblingData) => siblingData.hasDiscount === true,
description: 'Discounted price',
},
}
Field with validation:
{
name: 'phone',
type: 'text',
validate: (value) => {
if (!value) return true
const phoneRegex = /^\+1\d{10}$/
if (!phoneRegex.test(value)) {
return 'Format: +1XXXXXXXXXX'
}
return true
},
}
Richtext with custom features:
import { lexicalEditor, LinkFeature, UploadFeature } from '@payloadcms/richtext-lexical'
{
name: 'content',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
LinkFeature({ enabledCollections: ['pages', 'posts'] }),
UploadFeature({ collections: { media: { fields: [{ name: 'caption', type: 'text' }] } } }),
],
}),
}
Custom Field Component
For displaying non-standard UI in admin panel, while data is stored normally:
// fields/ColorPicker/index.tsx (admin component)
'use client'
import { useField } from 'payload/components/forms'
const ColorPickerField = ({ path }: { path: string }) => {
const { value, setValue } = useField<string>({ path })
const colors = ['#FF5733', '#33FF57', '#3357FF', '#FF33A8', '#33A8FF']
return (
<div className="field-type">
<label className="field-label">Color</label>
<div style={{ display: 'flex', gap: 8 }}>
{colors.map(color => (
<div
key={color}
onClick={() => setValue(color)}
style={{
width: 32,
height: 32,
borderRadius: '50%',
background: color,
cursor: 'pointer',
border: value === color ? '3px solid #000' : '2px solid transparent',
}}
/>
))}
</div>
<input
type="text"
value={value || ''}
onChange={e => setValue(e.target.value)}
placeholder="#000000"
style={{ marginTop: 8 }}
/>
</div>
)
}
export default ColorPickerField
// collections/Products.ts — connecting custom component
{
name: 'brandColor',
type: 'text',
admin: {
components: {
Field: '/fields/ColorPicker/index#ColorPickerField',
},
},
}
Blocks Field (flexible page builder)
// blocks/TextBlock.ts
import { Block } from 'payload/types'
const TextBlock: Block = {
slug: 'textBlock',
labels: { singular: 'Text Block', plural: 'Text Blocks' },
fields: [
{ name: 'content', type: 'richText' },
{
name: 'columns',
type: 'select',
options: [
{ label: '1 column', value: '1' },
{ label: '2 columns', value: '2' },
],
defaultValue: '1',
},
],
}
const ImageBlock: Block = {
slug: 'imageBlock',
fields: [
{ name: 'image', type: 'upload', relationTo: 'media', required: true },
{ name: 'caption', type: 'text' },
{ name: 'fullWidth', type: 'checkbox', defaultValue: false },
],
}
// In Pages collection:
{
name: 'sections',
type: 'blocks',
blocks: [TextBlock, ImageBlock, CTABlock, GalleryBlock],
minRows: 1,
}
Rendering Blocks in Frontend
// components/Blocks.tsx
import type { Page } from '@/payload-types'
type BlockComponent = {
textBlock: React.FC<{ content: any; columns: string }>
imageBlock: React.FC<{ image: any; caption?: string; fullWidth: boolean }>
}
const blockComponents: BlockComponent = {
textBlock: ({ content, columns }) => (
<div className={`columns-${columns}`}>
<RichText content={content} />
</div>
),
imageBlock: ({ image, caption, fullWidth }) => (
<figure className={fullWidth ? 'full-width' : ''}>
<img src={image.url} alt={image.alt} />
{caption && <figcaption>{caption}</figcaption>}
</figure>
),
}
export const Blocks = ({ sections }: { sections: Page['sections'] }) => {
return (
<>
{sections?.map((block, i) => {
const Component = blockComponents[block.blockType as keyof BlockComponent]
if (!Component) return null
return <Component key={i} {...(block as any)} />
})}
</>
)
}
Virtual Fields (admin only)
{
name: 'fullName',
type: 'text',
admin: {
readOnly: true,
description: 'Auto-calculated',
},
hooks: {
afterRead: [
({ data }) => `${data?.firstName} ${data?.lastName}`.trim(),
],
},
}
Timeline
Developing a set of custom fields (3–5 non-standard types with components) — 2–3 days.







