Skip to content

Define the TipTap block's text block styles inside the text blocks - #6383

Merged
VPS-Andreas merged 15 commits into
mainfrom
tiptap-text-block-styles
Oct 6, 2026
Merged

VPS-Andreas merged 15 commits into
mainfrom
tiptap-text-block-styles

Conversation

@VPS-Andreas

@VPS-Andreas VPS-Andreas commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

A text block style had to be configured in two places: globally in textBlockStyles, and again through an appliesTo listing the text block types it was allowed for. Which styles a text block offers could only be read by scanning every style's appliesTo, and two text blocks sharing a tag could not be told apart at all.

A text block now carries its style definitions directly, and textBlockStyles is gone:

const headlineStyles: TipTapTextBlockStyle[] = [
    { name: "headline300", label: "Headline 300", element: (props, Tag) => <Tag {...props} /> },
    { name: "headline400", label: "Headline 400", element: (props, Tag) => <Tag {...props} /> },
];

createTipTapRichTextBlock({
    textBlocks: [
        { name: "heading-1", label: "Heading 1", tag: "h1", styles: headlineStyles },
        { name: "heading-2", label: "Heading 2", tag: "h2", styles: headlineStyles },
        { name: "display", label: "Display", tag: "h1", element: (props, Tag) => <Tag style={{ fontSize: 64 }} {...props} /> },
    ],
    orderedList: { styles: copyStyles },
});

Text blocks that offer the same style share its definition, which makes the shared set explicit instead of implying it through repeated names. A style's element receives the tag of the text block it is applied to, so one definition works for several tags. A text block that needs no style choice carries its own element instead of styles; the two exclude each other in the types, so a text block either offers a choice or renders one way, and the styling select is only shown where there is something to choose. Lists take the same shape instead of appliesTo: ["ordered-list", "unordered-list"].

The stored format is unchanged — a style is still identified by its name in textBlockStyle — so no migration is needed.

Second of four stacked pull requests, on top of #6382; #6384 adds defaultStyle, #6385 splits the block's Storybook stories by topic.

Example

The unit tests in createTipTapRichTextBlock.test.ts/.test.tsx, and in Storybook:

Session: https://claude.ai/code/session_f3c6ad37-c68b-4187-85b6-2abe5aa95c35

@VPS-Obi

VPS-Obi commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

We'll review this once #6382 (which is likely to change) has been merged.

@VPS-Obi
VPS-Obi marked this pull request as draft September 17, 2026 07:53
@VPS-Obi
VPS-Obi removed request for VPS-Obi and nsams September 17, 2026 07:58
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-styles branch 3 times, most recently from 528abb4 to b3586f0 Compare September 17, 2026 09:08
@VPS-Andreas
VPS-Andreas marked this pull request as ready for review September 17, 2026 10:00
@github-actions
github-actions Bot requested a review from VPS-Obi September 17, 2026 10:00
@VPS-Andreas
VPS-Andreas marked this pull request as draft September 17, 2026 10:00
@VPS-Obi
VPS-Obi removed their request for review September 17, 2026 10:01
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-styles branch from b3586f0 to 4d1dede Compare September 18, 2026 08:13
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-styles branch 2 times, most recently from 0810ac3 to f5b91b5 Compare September 21, 2026 08:13
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-styles branch from f5b91b5 to c92054e Compare September 21, 2026 14:30
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-styles branch 2 times, most recently from 49987f3 to da44477 Compare September 22, 2026 07:18
VPS-Obi
VPS-Obi previously approved these changes Sep 24, 2026
Comment thread packages/api/cms-api/src/blocks/tipTap/tipTapValidation.ts Outdated
Comment thread packages/api/cms-api/src/blocks/tipTap/textBlocks.ts Outdated
VPS-Andreas and others added 3 commits September 30, 2026 10:22
The editor collected the styles of every text block and list into one list, keyed by
name - the global form this pull request replaces. The node view rendered a name through
whichever definition came first, which is why reusing a name with a different definition
had to be rejected.

The node view now takes the style from the node's own text block, or from the list the
node sits in, the way the toolbar already offers them. Two text blocks can therefore
define a name each their own way, and nothing has to be rejected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The only thing the API wants to know is whether anything offers a style at all, which
decides whether the text block node carries a `textBlockStyle` attribute. It read that
off the length of a list of all styles, collected and keyed by name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Next to `orderedList` and `unorderedList`, the options took the list the walk is inside
and whether it is inside a list item - state the function passes to itself, which a
caller had to tell apart from the configuration. `list` was even reassigned from the two
list options along the way. An inner function now walks the content and keeps the
configuration in its closure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VPS-Obi
VPS-Obi previously approved these changes Oct 5, 2026
…oolbar too

The node view renders a list item's content with the styles of the innermost list it
sits in, and the API validates it against that same list. The toolbar asked
`isActive("orderedList")` first, which matches any ancestor: inside a bullet list
nested in an ordered one it offered the outer list's styles, wrote a style the inner
list doesn't offer, and left the editor with a style that renders as none and content
the API rejects.

The nearest-ancestor lookup moves out of the node view so the toolbar resolves the same
list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread packages/api/cms-api/src/blocks/tipTap/textBlocks.ts Outdated
* If none is specified, the inline style is allowed everywhere.
*/
appliesTo?: TipTapTextBlockType[];
appliesTo?: string[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope: Now that text block styles live inside text blocks, it's odd that inline styles specific to a text block don't. We could consider moving them into the text blocks config in a follow-up PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and worth doing - appliesTo is the last place where something is tied to a text block from the outside. A follow-up, not this stack, which is four pull requests deep already.

One thing that makes it more than a move of the same shape: an inline style is a mark, not a node attribute. A mark spans a selection and survives a type switch, so a text block can end up carrying one it doesn't offer - the case we just had to handle for textBlockStyle. And the API validates appliesTo (containsInvalidInlineStyleMarks), so content that is fine today can become invalid. Both are solvable, but it needs a decision on what happens to a mark whose text block no longer offers it, rather than only moving the configuration.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, seems better to leave it like this.

`resolveList` mirrored the Admin's signature and recorded whether a list is an `ol` or a
`ul`. The Admin needs that to toggle and render the list; the API reads it nowhere, and
which tag a list has follows from the option it was configured as.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@VPS-Andreas
VPS-Andreas requested a review from VPS-Obi October 5, 2026 15:48
@VPS-Andreas
VPS-Andreas merged commit e84ba03 into main Oct 6, 2026
18 checks passed
@VPS-Andreas
VPS-Andreas deleted the tiptap-text-block-styles branch October 6, 2026 15:14
SebiVPS added a commit that referenced this pull request Oct 7, 2026
…s text block

#6383 removed the `textBlockStyles` option, but this story still used it,
so `cms-admin` no longer passed the type check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
VPS-Obi added a commit that referenced this pull request Oct 7, 2026
…6523)

The lint workflow fails on main because the TipTap table debug story
from #6477 configures its rich text block with `textBlockStyles`, which
#6383 replaced with styles defined inside the text blocks. Both pull
requests were merged independently, so the type check only breaks on
main.

---
_Generated by [Claude
Code](https://claude.ai/code/session_0151DMC1snZUQ4g3CmW6DhRY)_

Co-authored-by: Claude <noreply@anthropic.com>
VPS-Obi pushed a commit that referenced this pull request Oct 8, 2026
The styling select always offered a "Default" entry standing for "no
style", even where a design has no unstyled variant and every text block
is meant to carry one of the configured styles. Editors had to pick the
right style by hand on every new text block, and forgetting it produced
unstyled content.

A text block (or list) with a `defaultStyle` has no such state:

```tsx
createTipTapRichTextBlock({
    textBlocks: [
        { name: "heading-1", label: "Heading 1", tag: "h1", styles: headlineStyles, defaultStyle: "headline300" },
        { name: "heading-2", label: "Heading 2", tag: "h2", styles: headlineStyles, defaultStyle: "headline400" },
    ],
});
```

The select drops its "Default" entry, and the style is applied to new
content and to every text block the editor creates without one —
pressing Enter at the end of a text block, the `Mod-Alt-<level>`
shortcuts, pasting. Rather than reimplementing each of those paths, a
ProseMirror plugin fills in a missing style on document change, so
opening older content leaves it untouched and does not mark the document
as changed. Switching the type keeps a style the new text block also
offers and falls back to its `defaultStyle` otherwise, and toggling a
list hands the text block to the list's styles or back — through the
toolbar buttons and `Mod-Shift-7`/`Mod-Shift-8` alike.

Because the default sits on the text block rather than on a tag, two
text blocks sharing a tag can have different defaults, and a text block
without one keeps its "Default" entry next to text blocks that have one.

Third of four stacked pull requests, on top of #6383 and #6382; #6385
splits the block's Storybook stories by topic.

## Example

`createTipTapRichTextBlock.test.ts`/`.test.tsx` cover the validation and
the content a default style produces. The Storybook story [Default Text
Block
Style](https://tiptap-text-block-default-style--69df3371c46abe69b5199825.chromatic.com/?path=/story/blocks-tiptaprichtextblock--default-text-block-style)
walks through a text block with a default, one without, and the styled
lists.

Session:
https://claude.ai/code/session_f3c6ad37-c68b-4187-85b6-2abe5aa95c35

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants