This article explores how to create a reusable and flexible field for managing section layouts in Payload CMS. The approach aims to make layout configuration straightforward for developers and user-friendly for content managers.

If you only want to check the code: field in Payload, frontend implementation, GitHub.

Don't be confused

I use Payload 3.0, but you can use previous versions.The part related to managing section paddings will be the same.

Creating a reusable field

If you are familiar with Payload, you know that blocks are used for sections here. This is a powerful tool that allows you to perform validation on the frontend, make some fields available or visible at certain values of other fields, use your own components, and much more.  More information can be found in the docs.

The simplest implementation of a universal section padding control would be to create and use such a const.

012345678910111213141516171819202122import { Field } from 'payload'; export const SECTION_LAYOUT: Field = { type: 'group', name: 'sectionLayout', label: 'Section layout', fields: [ { type: 'number', name: 'paddingTop', label: 'Padding top', defaultValue: 24, required: true, }, { type: 'number', name: 'paddingBottom', label: 'Padding Bottom', defaultValue: 24, required: true, }, ], };

So, we created constant SECTION_LAYOUT,which is Field with type group and fields paddingTop and paddingBottom. As simple as it can be. The group type was chosen because it adds a field to the schema as opposed to collapsible, which just unites fields into a collapsible group on the frontend part of the admin panel. Also group allows to work more conveniently with types.

This is how it looks like in admin panel:

Simple section layout with only two settings: padding top and bottom.
Simple section layout with only two settings: padding top and bottom.

Adding breakpoints

The next step is adding the ability to add breakpoints. This is necessary to build responsive web sites. I like the idea of adding a pre-populated breakpoint and the ability to add your own breakpoints at the same time. This will make filling in content both easy and flexible.

01234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465import { Field, NumberFieldSingleValidation } from 'payload'; const minValueValidator: NumberFieldSingleValidation = (value: number | null | undefined) => Number(value) >= 0 ? true : 'Value must be equal or greater than 0'; export const SECTION_LAYOUT: Field = { type: 'group', name: 'sectionLayout', label: 'Section layout', fields: [ { type: 'number', name: 'paddingTop', label: 'Padding top', defaultValue: 24, required: true, validate: minValueValidator, }, { type: 'number', name: 'paddingBottom', label: 'Padding Bottom', defaultValue: 24, required: true, validate: minValueValidator, }, { type: 'array', name: 'breakpoints', label: 'Breakpoints', fields: [ { type: 'number', name: 'minWidth', label: 'Min width', required: true, validate: minValueValidator, }, { type: 'number', name: 'paddingTop', label: 'Padding top', defaultValue: 24, required: true, validate: minValueValidator, }, { type: 'number', name: 'paddingBottom', label: 'Padding Bottom', defaultValue: 24, required: true, validate: minValueValidator, }, ], defaultValue: [ { minWidth: 767, paddingTop: 48, paddingBottom: 48, }, ], }, ], };

So, now 1 breakpoint for min width 767 is added by default, and the user can add more breakpoints if needed. Also, now all fields are required, and the user can't add negative values. In such a case, he will see such an error:

Section layout with breakpoints and evalidation error message
Section layout with breakpoints and evalidation error message

Final touches

The current result can already be used on live projects, but I would prefer to add a few changes:

  • Add units to all settings. (It is obvious that these are pixels, but just because it is obvious to a developer doesn't mean it will be obvious to a content manager 🙂).
  • Add the ability to override defaultValues for specific sections (e.g. the Hero section is most often first, and it might be a good idea to remove the top default paddings). It's not a big deal, but why not?
  • Add a generated type to use on the frontent part. This will make it easier to develop a component to render the section.
  • Add a “Hide section” checkbox - this has nothing to do with managing paddings, but since we are already adding global settings to sections, it makes sense to leave this checkbox here. The idea of this customization is inspired by Shopify. The concept is that by checking this checkbox, the section should not be displayed, but it is still stored in the database, so it can be returned in 1 click. This is useful when you need to temporarily hide a section and then return it.

So, after adding all these features we get this code:

0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101import { Field, NumberFieldSingleValidation } from 'payload'; import { SectionLayout } from '@/payload-types'; const minValueValidator: NumberFieldSingleValidation = (value: number | null | undefined) => Number(value) >= 0 ? true : 'Value must be equal or greater than 0'; const rewriteFieldDefaults = ( fields: Field[], overrideDefaults?: Partial<SectionLayout>, ): Field[] => { if (!overrideDefaults) { return fields; } return fields.map((field) => { if (!('name' in field)) { return field; } const defaultValueFromOverrides = overrideDefaults[field.name as keyof SectionLayout]; if (defaultValueFromOverrides !== undefined) { if (field.type !== 'ui') { field.defaultValue = defaultValueFromOverrides; } } return field; }); }; export const SECTION_LAYOUT = (overrideDefaults?: Partial<SectionLayout>): Field => { const fields: Field[] = [ { type: 'checkbox', name: 'hideSection', label: 'Hide section', defaultValue: false, }, { type: 'number', name: 'paddingTop', label: 'Padding top, px', defaultValue: 24, required: true, validate: minValueValidator, }, { type: 'number', name: 'paddingBottom', label: 'Padding Bottom, px', defaultValue: 24, required: true, validate: minValueValidator, }, { type: 'array', name: 'breakpoints', label: 'Breakpoints', fields: [ { type: 'number', name: 'minWidth', label: 'Min width, px', required: true, validate: minValueValidator, }, { type: 'number', name: 'paddingTop', label: 'Padding top, px', defaultValue: 24, required: true, validate: minValueValidator, }, { type: 'number', name: 'paddingBottom', label: 'Padding Bottom, px', defaultValue: 24, required: true, validate: minValueValidator, }, ], defaultValue: [ { minWidth: 767, paddingTop: 48, paddingBottom: 48, }, ], }, ]; return { type: 'group', name: 'sectionLayout', label: 'Section layout', interfaceName: 'sectionLayout', fields: rewriteFieldDefaults(fields, overrideDefaults), }; };

Adding units is the easiest part of the change, it's basically just changing the label. Adding the “Hide section” checkbox I think it's obvious, and we don't have to stop there.

I added a part of payload-types.ts with an autogenerated SectionLayout type to make it clear what we get in the output. The type is generated because of the added setting interfaceName.

The most global change is the ability to override defaultValues for specific sections. To do this, const SECTION_LAYOUT has been changed to a function that can accept overrideDefaults. You can find out the structure of this parameter by looking at the generated type on tab 3. By the way, this is another place where the interfaceName setting comes in handy.

In the rewriteFieldDefaults function, you may be confused by such lines as if ((!('name' in field))) { return field; } and if (field.type !== 'ui') {...}. field may not have a name for certain types, such as collapsible. Such fields are not transformed into values in the database, but just improve the UI in admin panel. The same applies to a field with type ui - it cannot have a defaultValue, and therefore there is no need to overwrite it.

So, the final view of the newly added Hero section (see Table 2) looks like this:

Section layout final view
Section layout final view

Frontend implementation

Now, the only thing left is to implement the frontend part. I will do it using React component since using Next makes working with Payload much more convenient and simple since Payload 3. You can implement using the technology your site uses.

The idea is to create a Section component that will take the settings created earlier and render the section with the selected paddings on the selected breakpoints. Or render nothing if the “Hide section” checkbox is checked.

01234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859import { 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, children, ...rest }: SectionProps) => { if (hideSection) { return null; } const id = rest.id ?? useId(); const initialStyles = ` [id="${id}"] { padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px; } `; const styles = initialStyles + (breakpoints ? breakpoints.map((breakpoint) => generateMediaQuery(id, breakpoint)).join('\n') : ''); return ( <section {...rest} id={id}> <style>{styles}</style> {children} </section> ); };

So, let's start with the Section component. It accepts SectionLayout type settings as prop (described in the article settings) and also ComponentProps<'section'> - React props for <section> tag such as className, id, style, etc. They also include children.

If “Hide section” checkbox is checked - return null, we don't show the section. I think this part is very clear.

In order to add styles for the section, the <style> tag is used. We can't use props style because we use breakpoints.

The id is used as a selector because it must be unique. React hook useId is used for generating unique IDs if id has not been passed as prop. This is done in case we need to have a specific id. For example, a section should have an anchor link and the id field is added to settings. Why is the id selector used as an attribute ([id=“${id}”]) instead of the more primitive number sign (#${id})? The point is what unique ID is generated by useId. In my case it was :S1: - CSS sees ":" as a selector (e.g. :first-child), so the #:S1: selector is not correct and will be ignored.

The second tab shows the usage of Section - everything is simple enough you just need to pass sectionLayout from the block settings.

Section layout result on the web page
Section layout result on the web page

Conclusion

In this article, we built a reusable Payload field and a React-based frontend implementation for managing section layouts. This solution provides features like section visibility toggles, customizable paddings with breakpoints, and override options for default values, all while maintaining scalability and ease of use.

Video demonstration of changing paddings in admin panels from Live Preview:

Section layout work presentation
Section layout work presentation

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