Skip to content

Add defaultStyle to a TipTap text block - #6384

Open
VPS-Andreas wants to merge 10 commits into
mainfrom
tiptap-text-block-default-style
Open

VPS-Andreas wants to merge 10 commits into
mainfrom
tiptap-text-block-default-style

Conversation

@VPS-Andreas

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

Copy link
Copy Markdown
Contributor

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:

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

@VPS-Obi

VPS-Obi commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

We'll review this once #6383 has been merged.

@VPS-Obi
VPS-Obi marked this pull request as draft September 17, 2026 07:52
@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-default-style branch 2 times, most recently from 3ac786e to 7695789 Compare September 17, 2026 08:36
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-default-style branch from 7695789 to 709b81b Compare September 17, 2026 09:08
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-default-style branch from 709b81b to 07ba087 Compare September 17, 2026 09:26
@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:02
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-default-style branch from 07ba087 to cf07062 Compare September 18, 2026 08:13
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-default-style branch from cf07062 to eb97bbb Compare September 21, 2026 07:17
@greptile-apps

greptile-apps Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Adds optional default styling to text blocks in the rich text editor.

The PR appears safe to merge based on the reviewed changes.

Summary

Adds configurable default styles for TipTap text blocks and lists, including editor-side style reconciliation and matching API validation.

  • The follow-up changes reconcile styles per selected block and correct incompatible styles created by shortcuts and other editor actions.
  • The previously reported findings are resolved or withdrawn; no new actionable issue was established.

Reviews (3) · Last reviewed commit: "cms-api: Pass the DraftJS list item's pa..."

Comment thread packages/admin/cms-admin/src/blocks/tipTap/extensions/DefaultTextBlockStyle.ts Outdated
Comment thread packages/admin/cms-admin/src/blocks/tipTap/TipTapToolbar.tsx Outdated
Comment thread packages/admin/cms-admin/src/blocks/tipTap/extensions/DefaultTextBlockStyle.ts Outdated
@VPS-Andreas
VPS-Andreas marked this pull request as draft October 5, 2026 11:29
@VPS-Andreas
VPS-Andreas marked this pull request as ready for review October 5, 2026 11:48
Comment thread packages/admin/cms-admin/src/blocks/tipTap/extensions/TextBlockList.ts Outdated
Comment thread packages/api/cms-api/src/blocks/tipTap/migrations/convertDraftJsToTipTap.ts Outdated
Comment thread packages/admin/cms-admin/src/blocks/tipTap/TipTapToolbar.tsx
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-default-style branch from 97283f1 to 785125b Compare October 5, 2026 13:21
Base automatically changed from tiptap-text-block-styles to main October 6, 2026 15:14
VPS-Andreas added a commit that referenced this pull request Oct 6, 2026
…6383)

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:

```tsx
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:

- [Text Block
Styles](https://tiptap-text-block-styles--69df3371c46abe69b5199825.chromatic.com/?path=/story/blocks-tiptaprichtextblock--text-block-styles)
- [Text Block Style
Interactions](https://tiptap-text-block-styles--69df3371c46abe69b5199825.chromatic.com/?path=/story/blocks-tiptaprichtextblock--text-block-style-interactions)
- [List Text Block
Styles](https://tiptap-text-block-styles--69df3371c46abe69b5199825.chromatic.com/?path=/story/blocks-tiptaprichtextblock--list-text-block-styles)
- [Text Block
Element](https://tiptap-text-block-styles--69df3371c46abe69b5199825.chromatic.com/?path=/story/blocks-tiptaprichtextblock--text-block-element)

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
VPS-Andreas and others added 6 commits October 6, 2026 17:14
The styling select always offered a "Default" entry standing for "no style", even
where a design has no unstyled variant. A text block with a defaultStyle drops
that entry, so every paragraph or heading of that type carries one of its styles.

The editor creates text blocks in several ways that don't go through the toolbar,
so a ProseMirror plugin fills the style in for any node that has none, rather
than reimplementing Enter, the keyboard shortcuts and paste one by one. It runs
only on a document change, so opening older content leaves it untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Toggling a list through the toolbar hands the cursor's paragraph from its text
block to the list or back, so the styles of whichever now holds it apply.
Mod-Shift-7 and Mod-Shift-8 went through the list extensions' own shortcuts and
left the previous style in place, which the list may not offer at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only the toolbar's type select kept the style within what the new text block offers. The
`Mod-Alt-<level>` shortcuts and the `#` input rules change the text block alone, so a
paragraph styled "intro" turned into a heading that still named it: the editor rendered
it without a style, the styling select stood empty although a configured `defaultStyle`
leaves it no empty entry, and the API rejects that content.

The plugin that fills in a missing default style now corrects a style neither the text
block nor the list it sits in offers, which covers every path at once. Switching the
type inside a list keeps the list's style as well, instead of falling back to the new
text block's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Toggling a list or switching the type wrote one style onto every text block the
selection spans: `updateAttributes` applies the same value to all of them, and the value
came from whichever node the toolbar read the active style off. Selecting two paragraphs
styled differently and toggling a list left both with the style of one of them, even
where the list offers the other.

Each text block in the selection now resolves its style from the style it carries, so it
keeps one the list offers and falls back to a default only where it has to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`makeListItem` and `addListItem` grew past two positional parameters while the list's
styles were added, where the repository's TypeScript guideline asks for a single options
object - with three list-shaped arguments next to each other, the call sites read as
guesswork.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@VPS-Andreas
VPS-Andreas force-pushed the tiptap-text-block-default-style branch from 785125b to 2f97d30 Compare October 6, 2026 15:14
@VPS-Andreas

Copy link
Copy Markdown
Contributor Author

@greptileai review

@VPS-Andreas
VPS-Andreas requested review from VPS-Obi and nsams October 6, 2026 15:21
name: "textBlockList",

// Must beat the list extensions, whose shortcuts use the same keys.
priority: 1100,

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.

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.

No — textBlockList is an Extension, not a Node, so it contributes nothing to the schema: TipTap builds the schema from splitExtensions(...).nodeExtensions, and this priority only orders plugins and shortcuts. With #6520 merged into this branch, the running editor reports defaultType: textBlock and a node order starting with textBlock.

The keys don't overlap either: textBlock takes Mod-Alt-<level>, textBlockList takes Mod-Shift-7/Mod-Shift-8. The 1100 only has to beat @tiptap/extension-list, which registers bulletList, orderedList and listKeymap without a priority of their own, so at the default 100.

@VPS-Obi

VPS-Obi commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@VPS-Andreas please test this change in Demo as well and verify that saving is possible to prevent issues such as described here: #6520. Unfortunately, we can't test this end to end in Storybook alone.

VPS-Andreas and others added 2 commits October 7, 2026 13:09
`defaultStyle` had no example in Demo, so reviewers could only see it in Storybook, and
saving a text block that carries its default went untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@VPS-Andreas

Copy link
Copy Markdown
Contributor Author

Tested in Demo. defaultStyle had no example there, so the paragraph now has paragraph300 and both lists list300 — without that the run wouldn't touch the feature at all.

Saving works, and the content comes back unchanged after a reload. A paragraph gets its default filled in on the first edit and the styling select drops its "Default" entry; Mod-Shift-7 over two differently styled paragraphs gives each list item the list's default instead of collapsing them onto one style; Mod-Alt-1 drops a style the target text block doesn't offer; and switching the type inside a list lifts the block out and resolves the style for the text block it becomes, while the remaining item keeps the list's style.

Before #6520 was merged into this branch, saving did fail here: toggling a list at the end of the content left an empty bulletList behind, which the API rejected. With #6520 the trailing node is a text block again — and it gets the paragraph's default style — so saving goes through.

@VPS-Andreas
VPS-Andreas requested a review from VPS-Obi October 7, 2026 14:06

This branch has not been deployed

No deployments
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.

2 participants