Content Stats
A content section with an intro, a grid of icon features, and a stats list.
import type { Block } from 'payload'import { contentFields } from '@/blocks/shared/contentFields'import { iconField } from '@/blocks/shared/contentIcons'export const ContentStats: Block = { slug: 'contentStats', interfaceName: 'ContentStatsBlock', fields: [ // Shared content core (eyebrow, title, paragraphs). Variant-specific fields // follow; edit the shared shape in @/blocks/shared/contentFields. ...contentFields, { name: 'features', type: 'array', minRows: 2, maxRows: 6, admin: { initCollapsed: true, }, fields: [ iconField, { name: 'title', type: 'text', required: true, }, { name: 'description', type: 'textarea', required: true, }, ], }, { name: 'stats', type: 'array', minRows: 1, maxRows: 8, admin: { initCollapsed: true, }, fields: [ { name: 'value', type: 'text', required: true, }, { name: 'label', type: 'text', required: true, }, ], }, ], labels: { plural: 'Content Stats Blocks', singular: 'Content Stats', },}import React from 'react'import { ArrowRight } from 'lucide-react'import type { ContentStatsBlock as ContentStatsBlockData } from '@/payload-types'import { contentIcons } from '@/blocks/shared/contentIcons'import { Badge } from '@/components/ui/badge'import { cn } from '@/utilities/ui'type Props = ContentStatsBlockData & { id?: string className?: string disableInnerContainer?: boolean}export const ContentStatsBlock: React.FC<Props> = ({ className, disableInnerContainer, eyebrow, features, id, paragraphs, stats, 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-10', { 'mx-auto max-w-3xl': !disableInnerContainer, })} > <div className="flex 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">{title}</h2> {paragraphs && paragraphs.length > 0 ? paragraphs.map((paragraph, index) => ( <p className="text-lg leading-7 text-muted-foreground" key={paragraph.id ?? index}> {paragraph.text} </p> )) : null} </div> {features && features.length > 0 ? ( <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3"> {features.map((feature, index) => { const Icon = feature.icon ? contentIcons[feature.icon] : null return ( <div className="flex flex-col gap-2" key={feature.id ?? `${feature.title}-${index}`}> {Icon ? <Icon className="size-6" /> : null} <h3 className="text-lg font-medium tracking-heading">{feature.title}</h3> <p className="text-sm leading-6 text-muted-foreground">{feature.description}</p> </div> ) })} </div> ) : null} {stats && stats.length > 0 ? ( <ul className="flex flex-col gap-2 border-t border-border/70 pt-8 text-muted-foreground"> {stats.map((stat, index) => ( <li className="flex items-center gap-1.5" key={stat.id ?? `${stat.value}-${index}`}> <ArrowRight className="size-4 opacity-50" /> <span className="font-medium text-foreground">{stat.value}</span> {stat.label} </li> ))} </ul> ) : null} </div> </div> </section> )}import type { Field } from 'payload'/** * Shared field core for the Content component family. * * Every content variant (content-columns, content-image-lead, * content-feature-media, content-feature-split, content-showcase, * content-quote, content-community, …) spreads these heading fields first and * then appends its own variant-specific shape — the media upload, feature * list, pull quote, or avatar wall that differs per layout. Editing the shared * eyebrow/title/paragraphs shape here updates every installed content block at * once, so the family never drifts field-by-field across a repo. * * The body copy is a repeatable `paragraphs` array of plain textareas (the * tailark content sections lead with one or two short marketing paragraphs); * editors add or remove paragraphs without a rich-text dependency. * * Installed once per repo at `src/blocks/shared/contentFields.ts`; re-running * `payload-components add content-*` never overwrites a copy you have already edited. */export const contentFields: Field[] = [ { name: 'eyebrow', type: 'text', }, { name: 'title', type: 'text', required: true, }, { name: 'paragraphs', type: 'array', minRows: 1, maxRows: 4, admin: { initCollapsed: true, }, fields: [ { name: 'text', type: 'textarea', required: true, }, ], },]import type { Field } from 'payload'import type { LucideIcon } from 'lucide-react'import { Cpu, Gauge, Lock, Shield, Sparkles, Zap } from 'lucide-react'/** * Shared icon allowlist for the feature-bearing Content variants * (content-feature-media, content-feature-split, content-showcase). * * Payload cannot store a React component, so each feature item picks an icon * by name from this fixed allowlist (`iconField`, a `select`) and the frontend * looks the name up in `contentIcons` at render. Keeping the options and the * component map in one file means the editor choices can never drift from what * actually renders — add an icon by adding it to both `contentIconOptions` * and `contentIcons` together. * * Installed once per repo at `src/blocks/shared/contentIcons.ts`; re-running * `payload-components add content-*` never overwrites a copy you have already edited. */export const contentIconOptions = ['zap', 'cpu', 'lock', 'sparkles', 'gauge', 'shield'] as constexport type ContentIconName = (typeof contentIconOptions)[number]export const iconField: Field = { name: 'icon', type: 'select', options: [ { label: 'Zap', value: 'zap' }, { label: 'Cpu', value: 'cpu' }, { label: 'Lock', value: 'lock' }, { label: 'Sparkles', value: 'sparkles' }, { label: 'Gauge', value: 'gauge' }, { label: 'Shield', value: 'shield' }, ],}export const contentIcons: Record<ContentIconName, LucideIcon> = { zap: Zap, cpu: Cpu, lock: Lock, sparkles: Sparkles, gauge: Gauge, shield: Shield,}Installation
npx payload-components add content-statsCopy the files straight from the registry, then wire the Payload fragments by hand:
pnpm dlx shadcn@latest add https://www.payload-components.xyz/r/content-stats.jsonWhat it installs
Copies 4 source files into your project:
src/blocks/shared/contentFields.tssharedsrc/blocks/shared/contentIcons.tssrc/blocks/ContentStats/config.tssrc/blocks/ContentStats/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 |
contentFields.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 eyebrow, title, and paragraphs fields come from the shared Content family base
(src/blocks/shared/contentFields.ts); the features grid and stats list are specific to this
variant. Feature icons are chosen from the shared allowlist in src/blocks/shared/contentIcons.ts.
Prop
Type
Each item in features carries icon (a Lucide name: zap · cpu · lock · sparkles · gauge · shield),
title, and description. Each item in stats carries:
Prop
Type
Usage
ContentStats 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
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
content-columnscontent-image-leadcontent-feature-mediacontent-feature-splitcontent-showcasecontent-quotecontent-communitycontent-split-rowscontent-rowscontent-image-framecontent-statscurrentcontent-listcontent-list-columnscontent-list-icons