---
title: "Extending a tier"
description: "Change a tier's headings, label feature or feature set without forking it."
requested_language: cs
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/cs/payload/editor/extending-tiers
version: unreleased (main)
docs_index: https://docs.systhema.app/cs/llms.txt
---
> This page isn't translated yet. Showing English.


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.

```ts
import { slotLexicalEditor } from '@systhemaui/payload'

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

## `headings`

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:

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

## `label`

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

## `features`

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

```ts
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.

## `admin`

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:

```ts
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 managed

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

| Key          | Behaviour                                                                                                                                                                                                                                                                                                                                                                       |
| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tierMarker` | Always 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](https://docs.systhema.app/cs/payload/editor/text-states.md#a-text-state-is-an-inline-style). |
| `blocks`     | **Merged, 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.                                                                                                                                                                              |
| `systhemaAi` | Re-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.                                                                                                                                                                                       |
| `textState`  | Always 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 change

`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 editors

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:

```ts
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.
