Feature Bento
An asymmetric bento grid that leads with a featured cell and fills supporting cells.
import type { Block } from 'payload'import { featureFields } from '@/blocks/shared/featureFields'import { linkGroup } from '@/fields/linkGroup'export const FeatureBento: Block = { slug: 'featureBento', interfaceName: 'FeatureBentoBlock', fields: [ // Shared feature core (eyebrow, title, description). Variant-specific fields // follow; edit the shared shape in @/blocks/shared/featureFields. ...featureFields, { name: 'items', type: 'array', required: true, minRows: 3, maxRows: 6, admin: { initCollapsed: true, description: 'The first item leads the grid as the featured cell.', }, fields: [ { name: 'title', type: 'text', required: true, }, { name: 'description', type: 'textarea', required: true, }, ], }, linkGroup({ overrides: { admin: { initCollapsed: true, }, maxRows: 1, }, }), ], labels: { plural: 'Feature Bento Blocks', singular: 'Feature Bento', },}import React from 'react'import type { FeatureBentoBlock as FeatureBentoBlockData } from '@/payload-types'import { CMSLink } from '@/components/Link'import { Badge } from '@/components/ui/badge'import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'import { cn } from '@/utilities/ui'type Props = FeatureBentoBlockData & { id?: string className?: string disableInnerContainer?: boolean}export const FeatureBentoBlock: React.FC<Props> = ({ className, description, disableInnerContainer, eyebrow, id, items, links, title,}) => { return ( <section className={cn('container', className)} id={id ? `block-${id}` : undefined}> <div className="overflow-hidden rounded-frame border border-border/70 bg-card/35 px-6 py-10 sm:px-8 lg:px-12 lg:py-14"> <div className={cn('flex flex-col gap-8', { 'mx-auto max-w-6xl': !disableInnerContainer, })} > <div className="flex max-w-3xl flex-col gap-4"> {eyebrow ? ( <Badge variant="outline" className="w-fit rounded-full px-3 py-1 uppercase tracking-eyebrow"> {eyebrow} </Badge> ) : null} <h2 className="text-4xl font-medium tracking-display text-balance sm:text-5xl">{title}</h2> {description ? ( <p className="text-base leading-7 text-muted-foreground sm:text-lg">{description}</p> ) : null} </div> {items && items.length > 0 ? ( <div className="grid auto-rows-fr gap-4 sm:grid-cols-2 lg:grid-cols-3"> {items.map((item, index) => ( <Card key={item.id ?? `${item.title}-${index}`} className={cn('flex flex-col border-border/70 bg-background/85 shadow-none', { 'sm:col-span-2 lg:row-span-2': index === 0, })} > <CardHeader className="gap-3 p-5"> <CardTitle className="text-xl tracking-title">{item.title}</CardTitle> </CardHeader> <CardContent className="p-5 pt-0"> <CardDescription className="text-sm leading-7 text-muted-foreground"> {item.description} </CardDescription> </CardContent> </Card> ))} </div> ) : null} {links && links.length > 0 ? ( <div className="flex flex-col gap-3 sm:flex-row"> {links.map(({ link }, index) => ( <CMSLink key={index} appearance={link.appearance === 'outline' ? 'outline' : 'default'} {...link} /> ))} </div> ) : null} </div> </div> </section> )}import type { Field } from 'payload'/** * Shared field core for the Feature component family. * * Every feature variant (feature-grid-basic, feature-split, feature-bento, * feature-steps, …) spreads these section-heading fields first and then * appends its own variant-specific shape — the item array and CTA links, * which differ per layout. Editing the shared eyebrow/title/description here * updates every installed feature block at once, so the family never drifts * field-by-field across a repo. * * Installed once per repo at `src/blocks/shared/featureFields.ts`; re-running * `payload-components add feature-*` never overwrites a copy you have already edited. */export const featureFields: Field[] = [ { name: 'eyebrow', type: 'text', }, { name: 'title', type: 'text', required: true, }, { name: 'description', type: 'textarea', },]Installation
npx payload-components add feature-bentoCopy the files straight from the registry, then wire the Payload fragments by hand:
pnpm dlx shadcn@latest add https://www.payload-components.xyz/r/feature-bento.jsonWhat it installs
Copies 3 source files into your project:
src/blocks/shared/featureFields.tssharedsrc/blocks/FeatureBento/config.tssrc/blocks/FeatureBento/Component.tsx
…and makes 4 edits to wire the block into your project:
| Registers the block | src/collections/Pages/index.ts |
| Maps the renderer | src/blocks/RenderBlocks.tsx |
| Regenerates types | src/payload-types.ts |
| Regenerates the admin import map | src/app/(payload)/admin/importMap.js |
featureFields.ts is the shared field core for this family — every variant composes it. Editing it updates each installed block at once, and re-running an install never overwrites a copy you have changed.Re-running the install converges: it detects existing wiring, skips it, and records install state in .payload-components/state.json.
Content model
The first three fields come from the shared featureFields base; items and links are specific
to this variant.
Prop
Type
Each item in items carries:
Prop
Type
Usage
FeatureBento block to its layout.RenderBlocks on the frontend, fully typed — no extra wiring.Requirements
- Target
- payload-website-starter
- Payload
- v3
- Next.js
- 15 / 16
- shadcn UI
- badge, card
Your project must already expose components.json, src/payload.config.ts, src/blocks/RenderBlocks.tsx, src/collections/Pages/index.ts — the surfaces payload-components add patches. The CLI verifies this against the support matrix before touching anything.
In this family
feature-grid-basicfeature-splitfeature-bentocurrentfeature-steps