Payload Field Types Reference
Complete reference for all Payload field types with examples.
Text Field
import type { TextField } from 'payload'
const textField: TextField = {
name: 'title',
type: 'text',
required: true,
unique: true,
minLength: 5,
maxLength: 100,
index: true,
localized: true,
defaultValue: 'Default Title',
validate: (value) => Boolean(value) || 'Required',
admin: {
placeholder: 'Enter title...',
position: 'sidebar',
condition: (data) => data.showTitle === true,
},
}
When to use position: 'sidebar': Reserve the sidebar for short fields that
give quick insight into the content — status, category, author, publish date,
slug. Avoid it for fields that need horizontal space to be useful, like a
description, rich text content, or long text — those belong in the main document
area. (title above is shown in the sidebar only to demonstrate the option.)
Slug Field
Use the native slug field type for slugs — never hand-roll a
{ type: 'text', unique: true } field. useAsSlug is required (names the
source field; there is no 'title' default). Defaults to required, unique,
index, position: 'sidebar'. Optional server-side slugify fn.
{ name: 'slug', type: 'slug', useAsSlug: 'title' }
Full prop reference: https://payloadcms.com/docs/fields/slug
Rich Text (Lexical)
import type { RichTextField } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { HeadingFeature, LinkFeature } from '@payloadcms/richtext-lexical'
const richTextField: RichTextField = {
name: 'content',
type: 'richText',
required: true,
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
HeadingFeature({
enabledHeadingSizes: ['h1', 'h2', 'h3'],
}),
LinkFeature({
enabledCollections: ['posts', 'pages'],
}),
],
}),
}
Advanced Lexical Configuration
import {
BoldFeature,
FixedToolbarFeature,
HeadingFeature,
IndentFeature,
InlineToolbarFeature,
ItalicFeature,
LinkFeature,
OrderedListFeature,
TableFeature,
UnderlineFeature,
UnorderedListFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
// Global editor config with full features
export default buildConfig({
editor: lexicalEditor({
features: () => {
return [
UnderlineFeature(),
BoldFeature(),
ItalicFeature(),
OrderedListFeature(),
UnorderedListFeature(),
LinkFeature({
enabledCollections: ['pages'],
fields: ({ defaultFields }) => {
const defaultFieldsWithoutUrl = defaultFields.filter((field) => {
if ('name' in field && field.name === 'url') return false
return true
})
return [
...defaultFieldsWithoutUrl,
{
name: 'url',
type: 'text',
admin: {
condition: ({ linkType }) => linkType !== 'internal',
},
label: ({ t }) => t('fields:enterURL'),
required: true,
},
]
},
}),
IndentFeature(),
TableFeature(),
]
},
}),
})
// Field-specific editor with custom toolbar
const richTextWithToolbars: RichTextField = {
name: 'richText',
type: 'richText',
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [
...rootFeatures,
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
FixedToolbarFeature(),
InlineToolbarFeature(),
]
},
}),
label: false,
}
Relationship
import type { RelationshipField } from 'payload'
// Single relationship
const singleRelationship: RelationshipField = {
name: 'author',
type: 'relationship',
relationTo: 'users',
required: true,
maxDepth: 2,
}
// Multiple relationships (hasMany)
const multipleRelationship: RelationshipField = {
name: 'categories',
type: 'relationship',
relationTo: 'categories',
hasMany: true,
filterOptions: {
active: { equals: true },
},
}
// Polymorphic relationship
const polymorphicRelationship: PolymorphicRelationshipField = {
name: 'relatedContent',
type: 'relationship',
relationTo: ['posts', 'pages'],
hasMany: true,
}
Array
import type { ArrayField } from 'payload'
const arrayField: ArrayField = {
name: 'slides',
type: 'array',
minRows: 2,
maxRows: 10,
labels: {
singular: 'Slide',
plural: 'Slides',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
},
],
admin: {
initCollapsed: true,
},
}
Blocks
import type { BlocksField, Block } from 'payload'
const HeroBlock: Block = {
slug: 'hero',
interfaceName: 'HeroBlock',
fields: [
{
name: 'heading',
type: 'text',
required: true,
},
{
name: 'background',
type: 'upload',
relationTo: 'media',
},
],
}
const ContentBlock: Block = {
slug: 'content',
fields: [
{
name: 'text',
type: 'richText',
},
],
}
const blocksField: BlocksField = {
name: 'layout',
type: 'blocks',
blocks: [HeroBlock, ContentBlock],
}
Select
import type { SelectField } from 'payload'
// Use select for genuine taxonomy. For publish state, enable versions.drafts
// and rely on the auto-injected _status field instead of a custom select.
const selectField: SelectField = {
name: 'priority',
type: 'select',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
],
defaultValue: 'medium',
required: true,
}
// Multiple select
const multiSelectField: SelectField = {
name: 'tags',
type: 'select',
hasMany: true,
options: ['tech', 'news', 'sports'],
}
Upload
import type { UploadField } from 'payload'
const uploadField: UploadField = {
name: 'featuredImage',
type: 'upload',
relationTo: 'media',
required: true,
filterOptions: {
mimeType: { contains: 'image' },
},
}
Point (Geolocation)
Point fields store geographic coordinates with automatic 2dsphere indexing for geospatial queries.
import type { PointField } from 'payload'
const locationField: PointField = {
name: 'location',
type: 'point',
label: 'Location',
required: true,
}
// Returns [longitude, latitude]
// Example: [-122.4194, 37.7749] for San Francisco
Geospatial Queries
// Query by distance (sorted by nearest first)
const nearbyLocations = await payload.find({
collection: 'stores',
where: {
location: {
near: [10, 20], // [longitude, latitude]
maxDistance: 5000, // in meters
minDistance: 1000,
},
},
})
// Query within polygon area
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // closing point
]
const withinArea = await payload.find({
collection: 'stores',
where: {
location: {
within: {
type: 'Polygon',
coordinates: [polygon],
},
},
},
})
// Query intersecting area
const intersecting = await payload.find({
collection: 'stores',
where: {
location: {
intersects: {
type: 'Polygon',
coordinates: [polygon],
},
},
},
})
Note : Point fields are not supported in SQLite.
Join Fields
Join fields create reverse relationships, allowing you to access related documents from the "other side" of a relationship.
import type { JoinField } from 'payload'
// From Users collection - show user's orders
const ordersJoinField: JoinField = {
name: 'orders',
type: 'join',
collection: 'orders',
on: 'customer', // The field in 'orders' that references this user
admin: {
allowCreate: false,
defaultColumns: ['id', 'createdAt', 'total', 'currency', 'items'],
},
}
// From Users collection - show user's cart
const cartJoinField: JoinField = {
name: 'cart',
type: 'join',
collection: 'carts',
on: 'customer',
admin: {
allowCreate: false,
defaultColumns: ['id', 'createdAt', 'total', 'currency'],
},
}
Virtual Fields
import type { TextField } from 'payload'
// Computed from siblings
const computedVirtualField: TextField = {
name: 'fullName',
type: 'text',
virtual: true,
hooks: {
afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`],
},
}
// From relationship path
const pathVirtualField: TextField = {
name: 'authorName',
type: 'text',
virtual: 'author.name',
}
Do not set admin.useAsTitle to a computed field configured with
virtual: true. Computed virtual fields are not queryable, and Payload rejects
them as collection titles. Use a stored top-level field instead. The string-path
form, such as virtual: 'author.name', is queryable and can be used as
admin.useAsTitle when the title must come from a related document.
Conditional Fields
import type { UploadField, CheckboxField } from 'payload'
// Simple boolean condition
const enableFeatureField: CheckboxField = {
name: 'enableFeature',
type: 'checkbox',
}
const conditionalField: TextField = {
name: 'featureText',
type: 'text',
admin: {
condition: (data) => data.enableFeature === true,
},
}
// Sibling data condition (from hero field pattern)
const typeField: SelectField = {
name: 'type',
type: 'select',
options: ['none', 'highImpact', 'mediumImpact', 'lowImpact'],
defaultValue: 'lowImpact',
}
const mediaField: UploadField = {
name: 'media',
type: 'upload',
relationTo: 'media',
admin: {
condition: (_, { type } = {}) => ['highImpact', 'mediumImpact'].includes(type),
},
required: true,
}
Radio
Radio fields present options as radio buttons for single selection.
import type { RadioField } from 'payload'
const radioField: RadioField = {
name: 'priority',
type: 'radio',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
],
defaultValue: 'medium',
admin: {
layout: 'horizontal', // or 'vertical'
},
}
Row (Layout)
Row fields arrange fields horizontally in the admin panel (presentational only).
import type { RowField } from 'payload'
const rowField: RowField = {
type: 'row',
fields: [
{
name: 'firstName',
type: 'text',
admin: { width: '50%' },
},
{
name: 'lastName',
type: 'text',
admin: { width: '50%' },
},
],
}
Collapsible (Layout)
Collapsible fields group fields in an expandable/collapsible section.
import type { CollapsibleField } from 'payload'
const collapsibleField: CollapsibleField = {
label: ({ data }) => data?.title || 'Advanced Options',
type: 'collapsible',
admin: {
initCollapsed: true,
},
fields: [
{ name: 'customCSS', type: 'textarea' },
{ name: 'customJS', type: 'code' },
],
}
UI (Custom Components)
UI fields allow fully custom React components in the admin (no data stored).
import type { UIField } from 'payload'
const uiField: UIField = {
name: 'customMessage',
type: 'ui',
admin: {
components: {
Field: '/path/to/CustomFieldComponent',
Cell: '/path/to/CustomCellComponent', // For list view
},
},
}
Tabs & Groups
import type { TabsField, GroupField } from 'payload'
// Tabs
const tabsField: TabsField = {
type: 'tabs',
tabs: [
{
label: 'Content',
fields: [
{ name: 'title', type: 'text' },
{ name: 'body', type: 'richText' },
],
},
{
label: 'SEO',
fields: [
{ name: 'metaTitle', type: 'text' },
{ name: 'metaDescription', type: 'textarea' },
],
},
],
}
// Group (named)
const groupField: GroupField = {
name: 'meta',
type: 'group',
fields: [
{ name: 'title', type: 'text' },
{ name: 'description', type: 'textarea' },
],
}
Reusable Field Factories
Create composable field patterns that can be customized with overrides.
import type { Field, GroupField } from 'payload'
// Utility for deep merging
const deepMerge = <T>(target: T, source: Partial<T>): T => {
// Implementation would deeply merge objects
return { ...target, ...source }
}
// Reusable link field factory
type LinkType = (options?: {
appearances?: ('default' | 'outline')[] | false
disableLabel?: boolean
overrides?: Record<string, unknown>
}) => GroupField
export const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } = {}) => {
const linkField: GroupField = {
name: 'link',
type: 'group',
admin: {
hideGutter: true,
},
fields: [
{
type: 'row',
fields: [
{
name: 'type',
type: 'radio',
options: [
{ label: 'Internal link', value: 'reference' },
{ label: 'Custom URL', value: 'custom' },
],
defaultValue: 'reference',
admin: {
layout: 'horizontal',
width: '50%',
},
},
{
name: 'newTab',
type: 'checkbox',
label: 'Open in new tab',
admin: {
width: '50%',
style: {
alignSelf: 'flex-end',
},
},
},
],
},
{
name: 'reference',
type: 'relationship',
relationTo: ['pages'],
required: true,
maxDepth: 1,
admin: {
condition: (_, siblingData) => siblingData?.type === 'reference',
},
},
{
name: 'url',
type: 'text',
label: 'Custom URL',
required: true,
admin: {
condition: (_, siblingData) => siblingData?.type === 'custom',
},
},
],
}
if (!disableLabel) {
linkField.fields.push({
name: 'label',
type: 'text',
required: true,
})
}
if (appearances !== false) {
linkField.fields.push({
name: 'appearance',
type: 'select',
defaultValue: 'default',
options: [
{ label: 'Default', value: 'default' },
{ label: 'Outline', value: 'outline' },
],
})
}
return deepMerge(linkField, overrides) as GroupField
}
// Usage
const navItem = link({ appearances: false })
const ctaButton = link({
overrides: {
name: 'cta',
admin: {
description: 'Call to action button',
},
},
})
Field Type Guards
Type guards for runtime field type checking and safe type narrowing.
Type Guard Checks For Use When fieldAffectsDataField stores data (has name, not UI-only) Need to access field data or name fieldHasSubFieldsField contains nested fields (group/array/row/collapsible) Need to recursively traverse fields fieldIsArrayTypeField is array type Distinguish arrays from other containers fieldIsBlockTypeField is blocks type Handle blocks-specific logic fieldIsGroupTypeField is group type Handle group-specific logic fieldSupportsManyField can have multiple values (select/relationship/upload) Check for hasMany support fieldHasMaxDepthField supports population depth control Control relationship/upload/join depth fieldIsPresentationalOnlyField is UI-only (no data storage) Exclude from data operations fieldIsSidebarField positioned in sidebar Separate sidebar rendering fieldIsIDField name is 'id' Special ID field handling fieldIsHiddenOrDisabledField is hidden or disabled Filter from UI operations fieldShouldBeLocalizedField needs localization handling Proper locale table checks fieldIsVirtualField is virtual (computed/no DB column) Skip in database transforms tabHasNameTab is named (stores data) Distinguish named vs unnamed tabs groupHasNameGroup is named (stores data) Distinguish named vs unnamed groups optionIsObjectOption is {label, value} format Access option properties safely optionsAreObjectsAll options are objects Batch option processing optionIsValueOption is string value Handle string options valueIsValueWithRelationValue is polymorphic relationship Handle polymorphic relationships
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType } from 'payload'
function processField(field: Field) {
if (fieldAffectsData(field)) {
// Safe to access field.name
console.log(field.name)
}
if (fieldHasSubFields(field)) {
// Safe to access field.fields
field.fields.forEach(processField)
}
}
See FIELD-TYPE-GUARDS.md for detailed usage patterns.