Section
Keeps every block in rhythm. Wrap any Payload CMS block in a Section
and margin/padding logic is handled for you.
When to use
- Wrap every top‑level block you render one after another in your layout builder.
- Skip nested blocks—a parent
Section
already controls their spacing. - Stick to defaults; editors can tweak spacing via the admin dropdown if needed.
Section 1
Section 2
Section 3 with background + small spacing
Section 4
Section 5 with red background + mask
Section 6
Install
pnpm dlx [email protected] add https://p.livog.com/r/section.json
Code
import 'server-only'
import type { ComponentPropsWithRef, ReactNode } from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { selectSpacingOptions } from '@/payload/fields/section-padding'
import { cn } from '@/utils/cn'
export const sectionVariants = cva('isolate block', {
variants: {
spacing: Object.fromEntries(
selectSpacingOptions.map(option => [option, ''])
) as Record<(typeof selectSpacingOptions)[number], string>,
hasBackground: {
true: 'first:pt-18 relative bg-[var(--section-background)]',
false: 'first:mt-18'
},
accountForMask: {
true: '',
false: ''
},
compactToNext: {
true: '',
false: ''
},
compactToPrev: {
true: '',
false: ''
}
},
compoundVariants: [
// With background - padding
{ spacing: 'xxsmall', hasBackground: true, className: 'py-2 md:py-4' },
{ spacing: 'xsmall', hasBackground: true, className: 'py-4 md:py-8' },
{ spacing: 'small', hasBackground: true, className: 'py-8 md:py-16' },
{ spacing: 'medium', hasBackground: true, className: 'py-12 md:py-20' },
{ spacing: 'large', hasBackground: true, className: 'py-20 md:py-32' },
{ spacing: 'xlarge', hasBackground: true, className: 'py-24 md:py-36' },
// Without background - margin
{ spacing: 'xxsmall', hasBackground: false, className: 'my-4 md:my-8 first:mt-18 md:first:mt-24' },
{ spacing: 'xsmall', hasBackground: false, className: 'my-6 md:my-12 first:mt-18 md:first:mt-24' },
{ spacing: 'small', hasBackground: false, className: 'my-8 md:my-16 first:mt-18 md:first:mt-24' },
{ spacing: 'medium', hasBackground: false, className: 'my-12 md:my-20 first:mt-18 md:first:mt-24' },
{ spacing: 'large', hasBackground: false, className: 'my-20 md:my-32 first:mt-18 md:first:mt-24' },
{ spacing: 'xlarge', hasBackground: false, className: 'my-24 md:my-36 first:mt-18 md:first:mt-24' },
{ spacing: 'xsmall', hasBackground: false, accountForMask: true, className: '[&:has(+[data-has-mask])]:mb-4 [[data-has-mask]+&]:mt-4' },
{ spacing: 'small', hasBackground: false, accountForMask: true, className: '[&:has(+[data-has-mask])]:mb-6 [[data-has-mask]+&]:mt-6' },
{ spacing: 'medium', hasBackground: false, accountForMask: true, className: '[&:has(+[data-has-mask])]:mb-8 [[data-has-mask]+&]:mt-8' },
{ spacing: 'large', hasBackground: false, accountForMask: true, className: '[&:has(+[data-has-mask])]:mb-10 [[data-has-mask]+&]:mt-10' },
{ spacing: 'xlarge', hasBackground: false, accountForMask: true, className: '[&:has(+[data-has-mask])]:mb-12 [[data-has-mask]+&]:mt-12' },
// Compact spacing – separate margin (no background) vs padding (background)
{ compactToNext: true, hasBackground: true, className: 'pb-2 md:pb-2 [&+section]:pt-2' },
{ compactToNext: true, hasBackground: false, className: 'mb-2 md:mb-2 [&+section]:mt-2' },
{ compactToPrev: true, hasBackground: true, className: 'pt-2 md:pt-2 [section:has(+&)]:pb-2' },
{ compactToPrev: true, hasBackground: false, className: 'mt-2 md:mt-2 [section:has(+&)]:mb-2' }
],
defaultVariants: {
spacing: 'medium',
hasBackground: false,
accountForMask: true,
compactToNext: false,
compactToPrev: false
}
})
export type SectionVariants = VariantProps<typeof sectionVariants>
type SectionProps = ComponentPropsWithRef<'section'> & SectionVariants & { background?: ReactNode | string }
export const Section = ({ children, className, spacing, accountForMask = true, background, hasBackground, compactToNext, compactToPrev, ...props }: SectionProps) => {
const hasBackgroundClassName = typeof className === 'string' && /\bbg-[^\s]+/.test(className)
const hasMaskClassName = typeof className === 'string' && /\bmask-[^\s]+/.test(className)
const effectiveHasBackground = hasBackground || !!background || hasBackgroundClassName
const effectiveHasMask = accountForMask && hasMaskClassName
const style: React.CSSProperties = { ...(props.style || {}) }
if (typeof background === 'string') {
style['--section-background'] = background
}
return (
<section
data-has-background={effectiveHasBackground ? true : undefined}
data-has-mask={effectiveHasMask ? true : undefined}
className={cn(sectionVariants({ spacing, hasBackground: effectiveHasBackground, accountForMask, compactToNext, compactToPrev }), className)}
style={style}
{...props}>
{typeof background !== 'string' ? background : null}
{children}
</section>
)
}
Admin field
import { sectionSpacingField } from '@/payload/fields/section-padding'
export const Hero: Block = {
slug: 'hero',
fields: [
sectionSpacingField(), // dropdown: xxsmall → xlarge
// other fields…
],
}
Examples
Solid color background
<Section spacing="xsmall" background="#f7f7f7">
<Content />
</Section>
Custom background element
<Section spacing="medium" background={<GradientBlob />}>
<Content />
</Section>
Good to know
- Margin outside, padding inside when
background
is set. background
accepts a colour string or any React node.