If you’ve used Payload CMS 2.x or 1.x, you probably used the payload-better-fields-plugin library for color input. However, Payload CMS 3.0 introduced breaking changes to custom fields in the admin panel, rendering inputs from this library incompatible with the new version. While this issue may be addressed in future updates, for now, we will build a color input field from scratch.

If you are only interested in the code, check out the sections: field in Payload, custom field, frontend implementation, GitHub.

Creating reusable field

The first step is to create a reusable field. This will be based on a text field that incorporates a custom component for the admin panel.

0123456789101112131415161718import type { TextField } from 'payload'; export const COLOR = (overrides?: Omit<TextField, 'type'>): TextField => { const { name = 'color', label = 'Color', admin, ...rest } = overrides ?? {}; return { type: 'text', name, label, admin: { ...admin, components: { Field: '@/payload/components/ColorPicker/', }, }, ...rest, } as TextField; };

We define a COLOR function, which builds on TextField by enforcing the type as "text". This ensures consistency. The function accepts optional parameters, allowing you to customize settings like name, label, required, validate, defaultValue, etc. If no parameters are provided, it defaults to:

  • name: "color"
  • label: "Color"

Also, for the admin panel, custom component is used which is described below. In the previous article we did the same for inline rich text.

The second tab shows the use of the COLOR. The custom field sectionLayout described in this article is used as an example. In this way, we will add the ability to set the background color for all sections.

Building a custom field component

The next step is to create the custom component located at @/payload/components/ColorPicker/. This component combines two inputs:

  1. A color input (HTML <input type="color">)
  2. A text input (Payload CMS-provided TextInput component)

This setup allows users to set a color either by typing the value or selecting it from a color palet

012345678910111213141516171819202122232425262728293031323334'use client'; import { useField, TextInput } from '@payloadcms/ui'; import './styles.css'; const ColorPicker = ({ field: { label, required = false }, path, }: { field: { label: string; required?: boolean }; path: string; }) => { const { value, setValue } = useField<string>({ path }); return ( <div className={'color-picker'}> <label className={'field-label'}> {label} {required && <span className="required">*</span>} </label> <div className={'color-picker-row'}> <input type="color" value={value} onChange={(e) => setValue(e.target.value)} /> <TextInput label="" path={path} onChange={(e: { target: { value: string } }) => setValue(e.target.value)} value={value} /> </div> </div> ); }; export default ColorPicker;

The following props are passed automatically to the component:

  • path: the path to the customization (e.g., "layout.0.sectionLayout.backgroundColor"). It is used to get the current value and save its value after changes.
  • field: all field settings that were specified in fields/color.ts. In our case, we use label and required.

The useField hook provided by Payload CMS synchronizes the value between these inputs. Any changes made to either input are written back to the CMS seamlessly. The field’s label appears above the grid for a more user-friendly interface.

For a minimal and clean appearance, a small set of styles (shown in the second tab) is applied to ensure the component looks polished in the admin panel.

Appearance of the component in the admin panel:

Color input admin panel appearance
Color input admin panel appearance

Frontend implementation

Implementing the frontend is straightforward. The value retrieved from the settings is a simple text value, which can be directly applied to CSS styles.

Section.tsx
012345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061import { ComponentProps, useId } from 'react'; import { SectionLayout } from '@/payload-types'; type SectionProps = SectionLayout & ComponentProps<'section'>; const generateMediaQuery = ( id: string, breakpoint: NonNullable<SectionLayout['breakpoints']>[number], ): string => { if (!breakpoint) { return ''; } const { minWidth, paddingTop, paddingBottom } = breakpoint; return ` @media screen and (min-width: ${minWidth}px) { [id="${id}"] { padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px; } } `; }; export const Section = ({ hideSection = false, paddingTop, paddingBottom, breakpoints, backgroundColor, children, ...rest }: SectionProps) => { if (hideSection) { return null; } const id = rest.id ?? useId(); const initialStyles = ` [id="${id}"] { padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px; background-color: ${backgroundColor}; } `; const styles = initialStyles + (breakpoints ? breakpoints.map((breakpoint) => generateMediaQuery(id, breakpoint)).join('\n') : ''); return ( <section {...rest} id={id}> <style>{styles}</style> {children} </section> ); };

The setting was added to the Section component described in this article. It is used for demonstration purposes (see video below).

Conclusion

In this article, we implemented a color input field for Payload CMS 3. The result is:

  • A reusable field that integrates seamlessly with customization settings.
  • A color picker component that allows users to set colors either via text input or palette selection.

To see it in action, check out the accompanying video demonstration.

Video demonstration of color input field
Video demonstration of color input field

All the code from the article is available on GitHub. Feel free to use.