# Sharing Payload Fields Across a Component Family (/blog/shared-fields-across-component-families)

Author: Ducksss
Published: 2026-07-14T00:00:00.000Z



Structural variants often share the same opening vocabulary: eyebrow, title, and description, while
their item arrays and links express the structure. Copying the shared definitions into every Payload
config seems harmless until one label,
validation rule, or relationship target changes in only three of four places.

The durable pattern is a real shared source file that every consuming registry item ships. Each
variant spreads the common fields and adds only its structural requirements. The editor sees
consistent content language, while generated types still describe distinct block shapes.

<BlogFigure src="/blog/shared-fields-across-component-families/figure-01-shared-fields.svg" alt="The shared featureFields source module branching into four shipped configs: Feature Grid Basic, Feature Split, Feature Bento, and Feature Steps" caption="The shared source keeps one editorial vocabulary across four Feature configs; each variant remains a separate generated union member, renderer, and registry item." />

## Share meaning, not coincidence [#share-meaning-not-coincidence]

Two fields are worth sharing when they represent the same editorial concept and should evolve
together. Headings with identical name, label, validation, localization, and admin description are
a good candidate. Two unrelated text fields both called `title` may not be.

Premature sharing produces a generic helper full of options. If every consumer overrides the field
name, required state, admin copy, and validation, the abstraction is hiding differences rather than
capturing a contract.

Start with a family whose variants deliberately promise the same intro content. The
[variant-design guide](/blog/component-variants-without-prop-explosion) explains why structure stays
separate even when vocabulary is shared.

## Export a typed field array [#export-a-typed-field-array]

Payload exports field types and type guards. Define shared fields with real Payload types rather
than an untyped object array:

```ts
import type { Field } from 'payload'

export const featureFields: Field[] = [
  {
    name: 'eyebrow',
    type: 'text',
  },
  {
    name: 'title',
    type: 'text',
    required: true,
  },
  {
    name: 'description',
    type: 'textarea',
  },
]
```

Then compose the config explicitly. This excerpt uses the same imports and contract as the shipped
Feature Split config:

```ts
import type { Block } from 'payload'

import { featureFields } from '@/blocks/shared/featureFields'
import { linkGroup } from '@/fields/linkGroup'

export const FeatureSplit: Block = {
  slug: 'featureSplit',
  dbName: 'pc_fea_spl',
  interfaceName: 'FeatureSplitBlock',
  fields: [
    ...featureFields,
    {
      name: 'items',
      type: 'array',
      required: true,
      minRows: 2,
      maxRows: 6,
      admin: { initCollapsed: true },
      fields: [
        { name: 'title', type: 'text', required: true },
        { name: 'description', type: 'textarea', required: true },
      ],
    },
    linkGroup({
      overrides: {
        admin: { initCollapsed: true },
        maxRows: 2,
      },
    }),
  ],
  labels: { singular: 'Feature Split', plural: 'Feature Split Blocks' },
}
```

The spread keeps the final config readable. A reviewer can see the shared base and the exact
variant-specific contract without tracing a builder API.

Keep the export close to the family and name it after the shared concept. A global `commonFields.ts`
eventually becomes a dependency hub that unrelated components are afraid to change. Family-local
sharing gives the right blast radius: wide enough to prevent drift, narrow enough that a contributor
can review every consumer in one pass.

## Ship the shared module with every variant [#ship-the-shared-module-with-every-variant]

Source distribution changes the usual monorepo assumption. In this repository, a consumer may
install one variant without its siblings. Each of `feature-grid-basic`, `feature-split`,
`feature-bento`, and `feature-steps` therefore includes `shared/featureFields.ts` in its `files`
list with a target under the consumer's source tree.

The matching manifest must list the shared target as an owned file. Its component config import must
resolve from the installed locations. Tests install the variant in isolation, so an accidental
dependency on another family item fails before release.

Do not use `registryDependencies` for the internal module. shadcn resolves that field as another
public registry component. An internal filename such as `feature-fields` will be requested from a
registry and fail. The [registry guide](/blog/shadcn-registry-for-payload-cms) explains the ownership
categories.

## Keep generated interfaces distinct [#keep-generated-interfaces-distinct]

Spreading fields in the Payload config does not create TypeScript inheritance in the generated file,
and it does not need to. Payload produces a concrete interface or union member for each block. The
shared title and description appear in each generated shape, while each block keeps its own literal
discriminator.

That concrete output is useful. `FeatureGridBasicBlock`, `FeatureSplitBlock`, `FeatureBentoBlock`,
and `FeatureStepsBlock` remain four distinct union members even though their intro fields came from
one source array. The source module reduces config drift without weakening the discriminator that
drives rendering.

Run Payload's generator after adding or changing the shared fields. The official
[type generation documentation](https://payloadcms.com/docs/typescript/generating-types) remains the
source for generator setup. Inspect every affected union member in the diff; a shared change has a
deliberately broad generated impact.

## Evolve shared fields carefully [#evolve-shared-fields-carefully]

Changing a shared field changes every new document authored through every consuming variant. It may
also change validation or generated types for existing documents. Review the family as one migration
surface.

Renaming `description` to `copy` is not a local refactor. Existing stored data still uses the old
name unless migrated. Making the optional description required can invalidate old drafts. Changing a
relationship target can alter access and population behavior.

When only one variant needs different semantics, remove that field from the shared base for that
variant or declare a variant-specific field. Do not add conditionals to a supposedly shared array
until the caller API becomes harder to understand than a few repeated lines.

## Avoid mutating the shared array [#avoid-mutating-the-shared-array]

Treat exported field arrays as immutable configuration input. Do not import `featureFields` and push a
variant field into it; module evaluation order can make one config change another. Compose with
spreads and new arrays.

If a helper transforms fields, use Payload's exported field type guards when inspecting field kinds.
Avoid unsafe casts based only on the presence of a property. Payload fields form a union, and tabs,
rows, arrays, groups, and blocks carry different nested structures.

Simple shared modules are preferable to deep field factories. The goal is one stable vocabulary, not
a custom schema language layered over Payload.

## Keep docs and previews aligned [#keep-docs-and-previews-aligned]

Every component doc page has a hand-authored content-model table. Mark which fields come from the
shared family base and which belong to the variant. That helps users understand why sibling configs
look similar and where they may customize safely.

Demo twins use static representative content, so update them when a shared field rename changes the
source component. Visual class-mirror tests protect styling, while TypeScript and source tests protect
the data contract. The [component architecture docs](/docs/architecture) describe how shared source
travels with variants.

When an installed file set changes, also update the landing installation ledgers and component docs.
Hand-maintained file lists are easy to forget; release tests should compare registry, manifest, and
source wherever possible.

## Test isolation and family consistency [#test-isolation-and-family-consistency]

For each variant, test a clean install with no sibling present. Verify the shared file lands once,
imports resolve, the block registers, and a rerun is idempotent. Then install a second sibling and
confirm the shared file does not duplicate or conflict.

At the family level, check that common fields retain the same names, labels, validation, and order.
Test that variant-specific requirements remain distinct. Generate types after installing multiple
members and typecheck their renderers.

You can study an existing family's docs in the [component catalog](/components) and install one
representative structure:

```bash
npx payload-components add feature-bento
```

If a family repeats a field contract today, a good contribution includes the shared module, updated
registry and manifest file lists, all consuming imports, content-model documentation, and isolated
installer tests. That complete change turns copy-and-paste similarity into a durable, reviewable
source contract.
