🚧 Under development — suggestions for new items are always welcome! ✨

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.