Integration Connect
Integration logos wired to a central brand mark by connector lines, installed as a wired Payload block.
import type { Block } from 'payload'import { integrationFeaturedMark, integrationFields } from '@/blocks/shared/integrationFields'export const IntegrationConnect: Block = { slug: 'integrationConnect', interfaceName: 'IntegrationConnectBlock', fields: [ // Shared integration core (heading + subtext + integrations). Edit the shared // shape in @/blocks/shared/integrationFields to update every integration variant. ...integrationFields, // Variant-specific: a featured center brand mark the logos connect to. integrationFeaturedMark, ], labels: { plural: 'Integration Connect Blocks', singular: 'Integration Connect', },}import React from 'react'import type { IntegrationConnectBlock as IntegrationConnectBlockData } from '@/payload-types'import { Media } from '@/components/Media'import { cn } from '@/utilities/ui'type Props = IntegrationConnectBlockData & { id?: string className?: string disableInnerContainer?: boolean}export const IntegrationConnectBlock: React.FC<Props> = ({ className, disableInnerContainer, featuredLogo, heading, id, integrations, subtext,}) => { const items = integrations ?? [] const half = Math.ceil(items.length / 2) const left = items.slice(0, half) const right = items.slice(half) 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-14 sm:px-8 lg:px-12 lg:py-20"> <div className={cn('flex flex-col items-center gap-12', { 'mx-auto max-w-3xl': !disableInnerContainer, })} > <div className="relative flex w-full max-w-md items-center justify-between"> <div aria-hidden="true" className="pointer-events-none absolute inset-x-12 inset-y-0 bg-[radial-gradient(var(--connect-dots)_1px,transparent_1px)] [--connect-dots:rgba(120,120,130,0.35)] [background-size:16px_16px] opacity-60 [mask-image:radial-gradient(ellipse_60%_60%_at_50%_50%,#000_55%,transparent_100%)]" /> <div className="flex flex-col gap-6"> {left.map((item, index) => ( <div className="relative flex size-12 items-center justify-center rounded-xl border border-border/70 bg-background" key={item.id ?? `${item.name}-${index}`} > <Media resource={item.logo} imgClassName="size-6 w-auto object-contain" /> <span aria-hidden="true" className="pointer-events-none absolute left-full top-1/2 h-px w-10 bg-gradient-to-r from-border to-transparent" /> </div> ))} </div> {featuredLogo ? ( <div className="relative z-10 flex size-16 items-center justify-center rounded-2xl border border-foreground/25 bg-card shadow-lg"> <Media resource={featuredLogo} imgClassName="size-9 w-auto object-contain" /> </div> ) : null} <div className="flex flex-col gap-6"> {right.map((item, index) => ( <div className="relative flex size-12 items-center justify-center rounded-xl border border-border/70 bg-background" key={item.id ?? `right-${item.name}-${index}`} > <Media resource={item.logo} imgClassName="size-6 w-auto object-contain" /> <span aria-hidden="true" className="pointer-events-none absolute right-full top-1/2 h-px w-10 bg-gradient-to-l from-border to-transparent" /> </div> ))} </div> </div> <div className="flex max-w-lg flex-col items-center gap-5 text-center"> <h2 className="text-balance text-2xl font-semibold tracking-heading text-foreground sm:text-3xl"> {heading} </h2> {subtext ? ( <p className="text-pretty text-sm text-muted-foreground sm:text-base">{subtext}</p> ) : null} </div> </div> </div> </section> )}import type { Field } from 'payload'/** * Shared field core for the Integration block family. * * Every integration variant (integration-grid, integration-cluster, * integration-split, integration-connect, integration-orbit, integration-list, * integration-marquee, integration-testimonial, …) spreads these fields first * and then appends its own variant-specific shape (for example the cluster * variants add a featured center mark, and the testimonial variant adds a * quote). Editing the shared heading/integrations shape here updates every * installed integration block at once, so the family never drifts * field-by-field across a repo. * * Each integration is an editable Media upload plus an accessible name, an * optional supporting description, and an optional link, so editors manage the * wall of partner/tool logos from the admin instead of shipping hardcoded * brand SVGs. Logo-only variants simply ignore the per-item description/href. * * Installed once per repo at `src/blocks/shared/integrationFields.ts`; re-running * `payload-components add integration-*` never overwrites a copy you have already edited. */export const integrationFields: Field[] = [ { name: 'heading', type: 'text', required: true, }, { name: 'subtext', type: 'textarea', }, { name: 'integrations', type: 'array', required: true, minRows: 2, maxRows: 12, admin: { initCollapsed: true, }, fields: [ { name: 'logo', type: 'upload', relationTo: 'media', required: true, }, { name: 'name', type: 'text', required: true, }, { name: 'description', type: 'textarea', }, { name: 'href', type: 'text', }, ], },]/** * Optional center brand mark, spread by the variants that arrange the * integration logos around a focal point (cluster, split, connect, orbit, * marquee). Left empty, the variant renders without a center mark. */export const integrationFeaturedMark: Field = { name: 'featuredLogo', type: 'upload', relationTo: 'media', admin: { description: 'Optional center brand mark shown at the focal point of the integration layout.', },}Installation
npx payload-components add integration-connectCopy the files straight from the registry, then wire the Payload fragments by hand:
pnpm dlx shadcn@latest add https://www.payload-components.xyz/r/integration-connect.jsonWhat it installs
Copies 3 source files into your project:
src/blocks/shared/integrationFields.tssharedsrc/blocks/IntegrationConnect/config.tssrc/blocks/IntegrationConnect/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 |
integrationFields.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
heading, subtext, and integrations come from the shared integrationFields base. This variant
adds an optional featuredLogo the connector lines route to.
Prop
Type
Each item in integrations carries:
Prop
Type
Usage
IntegrationConnect 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
- none
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
integration-gridintegration-clusterintegration-splitintegration-connectcurrentintegration-orbitintegration-listintegration-marqueeintegration-testimonial