Docs

This page isn't translated yet

Extending a tier

Change a tier's headings, label feature or feature set without forking it.

On this page

Every factory (rootLexicalEditor, regionLexicalEditor, fragmentLexicalEditor, slotLexicalEditor) takes an optional overrides object. Use it when a tier is nearly right and forking it would mean re-deriving everything the tier already knows.

The motivating case: a product hero authored as ONE editor. It wants slot's vocabulary (paragraph, lead, small, links, inline button/chip/icon) plus a label and an h1, and it specifically must not offer sections, columns, lists or galleries.

import { slotLexicalEditor } from '@systhemaui/payload'

richTextField({
  name: 'hero',
  editor: slotLexicalEditor({ headings: ['h1'], label: true }),
})

headingsLink to this section

Replaces the tier's heading set, or removes headings entirely with false.

This is worth reaching for even when the content already renders correctly. A tier's toolbar heading dropdown is built from enabledHeadingSizes alone, so an h1 stored in a fragment editor (whose default set starts at h2) renders as an h1 but shows as the wrong level in the dropdown, and one click on that dropdown rewrites the page's only h1 with no way to restore it. If a tier holds a heading level, list it:

fragmentLexicalEditor({ headings: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] })

labelLink to this section

Adds or removes Systhema's Label feature. slot ships without one; root/region/fragment ship with one.

featuresLink to this section

Full control, mirroring Payload's own lexicalEditor({ features }) convention:

import { HeadingFeature } from '@payloadcms/richtext-lexical'
import { LabelFeature } from '@systhemaui/payload/lexical/features'

slotLexicalEditor({
  features: ({ defaultFeatures, tier }) => [
    ...defaultFeatures,
    HeadingFeature({ enabledHeadingSizes: ['h1'] }),
    LabelFeature(),
  ],
})

defaultFeatures is the editable part of the tier: everything it builds, including its BlocksFeature and any headings/label overrides you also passed. The four managed features below are not in it, and are applied after your transform returns. Systhema's own features are importable from @systhemaui/payload/lexical/features (LabelFeature, LeadFeature, SmallFeature, BalanceTextFeature).

Payload deduplicates features by key and the LAST one wins. Appending a second HeadingFeature REPLACES the tier's rather than adding to it. The snippet above works only because slot has no heading feature of its own. The same line on fragment would silently drop h2–h6. Prefer the headings option for that case.

adminLink to this section

Payload's own field-chrome options for the editor — hideGutter, placeholder, hideInsertParagraphAtEnd, hideAddBlockButton, hideDraggableBlockElement. root and inline keep the gutter; region, fragment and slot hide it, so a page whose hero is a slot field and whose body is a root field shows two differently-framed rich text fields. Put the gutter back on one of them with:

slotLexicalEditor({ admin: { hideGutter: false } })

The object is merged over the tier's own, key by key — the line above leaves hideInsertParagraphAtEnd: true in place. Unlike the four managed features, nothing is re-applied afterwards: these flags are presentation, so they override all the way back to Payload's defaults.

What stays managedLink to this section

Four features are re-applied after your override runs, so a tier cannot lose its identity no matter what the transform returns:

KeyBehaviour
tierMarkerAlways re-applied with this tier's name. Every block resolves its _tier/_inCard from this marker's DOM stamp, so an editor without it mis-gates every tier-conditional field inside it. It also marks the editor root systhema-editor-tokens — the scope to declare admin CSS variables into, see custom text states.
blocksMerged, never replaced. Whatever BlocksFeature you return keeps its own slugs and gains this tier's registered customBlocks. Without the merge, last-wins dedupe would silently drop them.
systhemaAiRe-derived from the features you actually ended up with, so the AI vocabulary matches the editor rather than the tier constant. An unmodified tier keeps its configured context verbatim.
textStateAlways re-applied from customTextStates.

To take a custom block OUT of a tier, narrow that block's own editor targeting in customBlocks rather than removing it here. That keeps the two definitions from disagreeing. Dropping BlocksFeature entirely logs a dev warning naming the blocks that went with it.

What extending does not changeLink to this section

slot is also the only tier with no AlignFeature, and that is deliberate: it is the single-line tier (captions, quote inners, hero descriptions), it ships the inline toolbar alone, and alignment applies to a block with nothing selected. Adding it means appending both AlignFeature() and FixedToolbarFeature() through features — at which point the field is a region editor wearing slot's tier marker, and fragmentLexicalEditor is usually the better starting point.

An extended editor keeps its base tier. slotLexicalEditor({ headings: ['h1'] }) is still the slot tier, so its content is stamped _tier: 'slot' and customBlocks/customTextStates targeting 'slot' still applies. That is what makes the extension safe, but it also means adding block-level blocks to a slot editor gives those blocks a tier their field gating was never designed around. The headings/label options avoid this entirely; the features transform does not.

The server stamps _tier too: the Pages and Posts collections walk each editor state from the tier of the field that owns it (read from the sanitized config), so inline blocks in a hero description are tagged slot rather than root.

Lazy mount on hand-rolled editorsLink to this section

The region/fragment/slot factories already lazy-mount their FieldComponent so nested editors below the fold don't render until they scroll into view (saves significant first-paint time on long admin pages). The root factory intentionally does NOT — it's always above the fold on a doc edit.

Reach for Extending a tier first. It keeps the tier marker, the custom blocks and the AI vocabulary wired up for you. If you genuinely need an editor built from scratch (lexicalEditor({ features: () => […] })), wrap the result with withLazyMount to get the same first-paint win:

import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { withLazyMount } from '@systhemaui/payload/lexical/editors'

const myLexicalEditor = () =>
  withLazyMount(
    lexicalEditor({
      features: () => [
        /* …your custom feature mix… */
      ],
    }),
  )

withLazyMount swaps the editor descriptor's FieldComponent.path to Systhema's lazy shell and rewires generateImportMap so the new path gets registered on the next payload generate:importmap. Everything else — server-side feature resolution, form state, the rendered editor UX once mounted — is unchanged.