From 1462f8caf0b501a6fe2eec5a5313c9a74085e6ba Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Fri, 7 Aug 2026 14:53:53 +0200 Subject: [PATCH 1/2] Improve custom formats for WYSIWYG docs --- content/guides/01.data-model/3.interfaces.md | 6 +- content/guides/01.data-model/4.rich-text.md | 242 ++++++++++++++++++ ...{4.relationships.md => 5.relationships.md} | 0 3 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 content/guides/01.data-model/4.rich-text.md rename content/guides/01.data-model/{4.relationships.md => 5.relationships.md} (100%) diff --git a/content/guides/01.data-model/3.interfaces.md b/content/guides/01.data-model/3.interfaces.md index a32f7d9e..dc17218c 100644 --- a/content/guides/01.data-model/3.interfaces.md +++ b/content/guides/01.data-model/3.interfaces.md @@ -96,13 +96,17 @@ The What You See Is What You Get (WYSIWYG) editor provides a text area with rich | Folder | Default folder to store uploaded files. Does not affect existing files. | | Soft Limit | Used to limit the number of characters within the Data Studio. | | Static Access Token | Token appended to asset URLs when displaying in the editor. | -| Custom Formats | JSON array of custom formatting styles to add to the editor's formatting menu. Only inline formats are supported; block, selector, and wrapper formats are ignored. | +| Custom Formats | JSON array of custom formatting entries added to the editor's Formats menu. Supports inline, block, selector, and grouped entries. `wrapper` entries are not supported. See [Rich Text](/guides/data-model/rich-text#custom-formats). | | Options Override | Deprecated and no longer applied. See the note below. | ::callout{icon="i-lucide-info"} **The editor's engine changed in Directus 12.** The WYSIWYG interface is now built on [Tiptap](https://tiptap.dev). Existing fields keep working without migration, but stored HTML is normalized to the editor's supported markup on first edit, and the **Options Override** (`tinymceOverrides`) option is deprecated. See the [Version 12 breaking changes](/releases/breaking-changes/version-12#wysiwyg-editor-rebuilt-on-tiptap) for details. :: +::callout{icon="i-lucide-book-open" color="primary" to="/guides/data-model/rich-text"} +Configure custom formats, understand what the editor stores, and style the output on your frontend in the [Rich Text](/guides/data-model/rich-text) guide. +:: + ### Markdown ![A markdown text editor with a toolbar with formatting options. Edit and preview tabs.](/img/b41d6822-35ab-48db-ada9-dd3a723a5c52.webp) diff --git a/content/guides/01.data-model/4.rich-text.md b/content/guides/01.data-model/4.rich-text.md new file mode 100644 index 00000000..58a199c6 --- /dev/null +++ b/content/guides/01.data-model/4.rich-text.md @@ -0,0 +1,242 @@ +--- +stableId: 2947492c-9b05-4261-b4dd-28ba4b0ceaba +title: Rich Text +description: Configure custom formats for the WYSIWYG interface, understand what the editor stores, and style the resulting HTML on your frontend. +--- + +The [WYSIWYG interface](/guides/data-model/interfaces#wysiwyg) stores its value as HTML. This page covers the **Custom Formats** option, which adds your own formatting entries to the editor, and what the stored HTML looks like so you can style it on your frontend. + +::callout{icon="i-lucide-info"} +The editor is built on [Tiptap](https://tiptap.dev) as of Directus 12. It stores only the markup its schema models. See [Version 12 breaking changes](/releases/breaking-changes/version-12#wysiwyg-editor-rebuilt-on-tiptap) for the supported HTML and how existing values are normalized. +:: + +## Custom Formats + +**Custom Formats** takes a JSON array of formatting entries. Set it on the field's interface options. When the array is not empty, a **Formats** dropdown is added to the editor toolbar. There is no toolbar option to enable it. + +The format is a subset of TinyMCE's `style_formats`, so existing configurations often carry over unchanged. + +Every entry needs a `title` and one of the following keys: + +| Key | Applies to | +| ---------- | --------------------------------------------------------------------------------------------- | +| `inline` | A tag wrapped around the selected text, such as `span`. | +| `block` | A single block tag. Converts the block when its tag differs. See [Block formats](#block-formats). | +| `selector` | One or more comma-separated block tags. Never converts the block. | +| `items` | An array of entries, grouped into a submenu. See [Grouped formats](#grouped-formats). | + +Each entry also takes optional `classes`, `styles`, and `attributes`. + +Every entry needs `classes` or `attributes` as well. The editor uses them to recognize the format again when the value is reloaded, so an entry without either is skipped. + +### Example + +```json +[ + { "title": "Lead paragraph", "block": "p", "classes": "lead" }, + { "title": "Subheading", "selector": "h2,h3", "classes": "subheading" }, + { + "title": "Callouts", + "items": [ + { "title": "Info", "block": "p", "classes": "callout callout-info" }, + { "title": "Warning", "block": "p", "classes": "callout callout-warning" } + ] + }, + { + "title": "Highlight", + "inline": "span", + "classes": "highlight", + "styles": { "background": "#ffff00" } + } +] +``` + +Applying **Lead paragraph** and **Highlight** produces: + +```html +

A lead paragraph.

+``` + +Define `lead`, `subheading`, `callout`, and `highlight` in your frontend's CSS. Directus stores the classes but does not style them. See [Styling the output on your frontend](#styling-the-output-on-your-frontend). + +### Inline formats + +An `inline` entry renders a tag around the selection, carrying its `classes`, its `attributes`, and its `styles` serialized into a `style` attribute: + +```json +{ + "title": "Highlight", + "inline": "span", + "classes": "highlight", + "styles": { "background": "#ffff00" }, + "attributes": { "title": "Highlighted text" } +} +``` + +```html +text +``` + +Inline entries are the only formats that write a `style` attribute into the content, so they are also the only ones with a visible effect inside the editor. The trade-off is an inline style in your stored HTML, which is harder to override from your frontend CSS. + +### Block formats + +A `block` entry applies to one block tag and takes a single tag only. Use `selector` for a list. + +```json +{ "title": "Lead paragraph", "block": "p", "classes": "lead" } +``` + +When the selected block's tag differs from the entry's tag, the block is converted. **Conversion works between paragraphs and headings only**, because those are the two block types that hold the same content. A `block: 'h2'` entry turns a paragraph into `

`, and a `block: 'p'` entry turns a heading back into a paragraph. + +Every other tag applies without converting. A `block: 'blockquote'` entry formats blocks that are already `
` and leaves everything else alone, and logs a warning in the browser console. Use `selector` when you never want conversion. + +### Selector formats + +A `selector` entry applies to the block tags you list and never changes the block's type: + +```json +{ "title": "Subheading", "selector": "h2,h3", "classes": "subheading" } +``` + +Applying this to an `

` or `

` adds the class. Applying it to a paragraph does nothing. + +### Grouped formats + +An `items` array groups entries into a submenu in the **Formats** dropdown: + +```json +{ + "title": "Callouts", + "items": [ + { "title": "Info", "block": "p", "classes": "callout callout-info" }, + { "title": "Warning", "block": "p", "classes": "callout callout-warning" } + ] +} +``` + +Groups support one level. A group nested inside another group is skipped, and a group whose entries are all invalid is dropped along with them. + +### Which tags a block format can target + +`block` and `selector` accept only tags the editor models as a node: + +`p`, `h1` through `h6`, `pre`, `blockquote`, `section`, `article`, `figure`, `figcaption`, `details`, `summary`, `dl`, `dt`, `dd`, `hr`, `img`, `ul`, `ol`, `li`, `table`, `tr`, `td`, `th`, `video`, `audio`, `iframe`. + +`div` and `span` are not on the list, so they cannot carry a block format. A `block: 'div'` entry is skipped. This matters when you migrate a TinyMCE configuration, since TinyMCE formats commonly wrap content in a `div`. It is the same reason a stored `
` is unwrapped when the editor loads it. Use a `section`, `article`, or `figure` entry instead, or move the wrapper into your frontend template. + +For a `selector` entry, one unsupported tag skips the whole entry. Split the tags across separate entries if you want the supported ones to keep working. + +### Invalid entries are skipped + +An entry the editor cannot build is skipped, and the rest of the array still loads. Every skipped entry logs a warning to the **browser console**. Nothing appears in the interface, so check the console when a format does not show up in the dropdown. + +Entries are skipped when they: + +- Have no `title`. +- Have none of `inline`, `block`, `selector`, or `items`. +- Have no `classes` and no `attributes`. +- Use `wrapper`, which is not supported. +- Name a tag the editor does not model, such as `div`. +- Pass a comma-separated list to `block` instead of `selector`. +- Nest a group inside another group. + +## Gotchas + +### The editor does not load your frontend CSS + +::callout{icon="i-lucide-triangle-alert" color="warning"} +The editor is not a preview of your site. Classes applied through custom formats have no styling attached in the Data Studio. +:: + +Directus loads no project stylesheet into the editing surface, so a class like `lead`, `float-left`, or `text-center` has no visual effect inside the WYSIWYG. The class is stored and round-trips correctly. It just has nothing styling it in the Data Studio. + +This surprises authors, who apply a format, see no change, and assume it is broken. Point them at the **Formats** dropdown instead: it shows the active format in its label and highlights the matching row. That is the confirmation that a format applied, not the rendered content. + +The editor's own content styles can also contradict your class. The editor styles `figure` as `display: table; margin: 0.8125rem auto`, so a captioned image with a `float-left` class renders centered in the editor and floated on your frontend. The same applies to anything depending on `display`, `float`, `width`, or a flex or grid context your frontend provides. + +There is currently no option to inject custom CSS into the editor. The deprecated **Options Override** (`tinymceOverrides`) option is no longer applied, so TinyMCE's `content_css` has no equivalent. + +Because of this, layout classes are the riskiest thing to hand to non-technical authors: no feedback in the editor, and a result that depends entirely on CSS the editor cannot see. Prefer semantic formats such as `lead`, `callout`, or `subheading` over layout ones where you can. + +### `styles` on a block entry only styles the dropdown + +On an `inline` entry, `styles` is written into the content as a `style` attribute. On a `block` or `selector` entry, `styles` only styles the entry's label inside the **Formats** dropdown. It is not written to the content. + +The editor does not round-trip `style` on block nodes. Only `class`, `id`, `title`, `role`, `lang`, `dir`, `data-*`, and `aria-*` survive a save and load cycle. TinyMCE applied block `styles` to the block itself, so this is a behavior change to account for when migrating. + +Use `classes` for block styling and define the CSS on your frontend. + +### Attributes on a block entry are filtered + +`attributes` on a `block` or `selector` entry accepts only `id`, `title`, `role`, `lang`, `dir`, `data-*`, and `aria-*`. Anything else, including `style` and `target`, is dropped with a console warning. + +`attributes: { "class": "…" }` is merged into `classes`, so toggling the format off removes those classes too. + +Inline entries do not filter `attributes`. + +## Toggling behavior + +Selecting an active format in the dropdown removes it. Applying and removing a block format behaves as follows: + +- Removing a format strips only the classes and attributes that format configured. Unrelated classes, `id`, `data-*`, and `aria-*` on the same block survive. +- The block keeps its tag. A `block: 'h2'` format does not turn the heading back into a paragraph when you remove it. +- A format applies to every eligible block in the selection as a single undo step. +- A format counts as active when every eligible block in the selection carries all of its classes, or all of its attributes for a format anchored on attributes alone. + +## Image captions + +Adding or editing an image opens a drawer with a **Caption** field. A non-empty caption wraps the image in a `
`: + +```html +
+ A wind turbine +
A wind turbine at dusk.
+
+``` + +Style `figure` and `figcaption` on your frontend to match. The editor centers them, which your own CSS will override. + +Behavior worth knowing: + +- Clearing the caption reverts to a bare ``, but only when the `
` holds nothing but images and carries no attributes of its own. A `
` you added deliberately is kept, and its caption is removed on its own. +- Editing an existing image updates it in place, so its attributes and its surrounding `
` survive the edit. +- Pressing `Enter` inside a caption leaves the figure and starts a new paragraph after it, rather than adding a second `
`. +- Pressing `Backspace` in an empty caption removes the caption. +- Deleting the image out of a figure removes the orphaned caption too. +- Stored `
` and `
` markup round-trips, including a caption placed before the image and a figure holding only a caption. + +## Styling the output on your frontend + +Directus stores the HTML. Rendering and styling it is your frontend's job. + +Add the classes your formats configure to your stylesheet: + +```css +.lead { + font-size: 1.25rem; + line-height: 1.6; +} + +.subheading { + color: #6644ff; + text-transform: uppercase; +} + +.callout { + border-left: 4px solid; + padding: 1rem; +} + +.callout-info { border-color: #3399ff; } +.callout-warning { border-color: #ffaa00; } +``` + +Two things to keep in mind: + +- Scope these styles so they do not collide with the rest of your site. Rendering the value inside a wrapper such as `.prose` and prefixing your selectors keeps author-applied classes from leaking. +- Review your selectors before upgrading to Directus 12 if you style stored HTML by tag, class, or attribute. Markup the editor does not model, such as `
` wrappers, is removed when a field is edited and saved. + +## Next Steps + +Read about the [WYSIWYG interface options](/guides/data-model/interfaces#wysiwyg), the [supported HTML and normalization behavior](/releases/breaking-changes/version-12#wysiwyg-editor-rebuilt-on-tiptap), and [keyboard shortcuts](/getting-started/accessibility) for the editor. diff --git a/content/guides/01.data-model/4.relationships.md b/content/guides/01.data-model/5.relationships.md similarity index 100% rename from content/guides/01.data-model/4.relationships.md rename to content/guides/01.data-model/5.relationships.md From 52d8b58f36308a95dc932245c91ec51335ba1631 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Thu, 13 Aug 2026 16:32:26 +0200 Subject: [PATCH 2/2] Update rich text documentation to clarify custom format usage and improve examples. Adjusted descriptions for better understanding of WYSIWYG interface behavior and entry requirements. --- content/guides/01.data-model/4.rich-text.md | 91 ++++++++++++++++++--- 1 file changed, 78 insertions(+), 13 deletions(-) diff --git a/content/guides/01.data-model/4.rich-text.md b/content/guides/01.data-model/4.rich-text.md index 58a199c6..7700b26e 100644 --- a/content/guides/01.data-model/4.rich-text.md +++ b/content/guides/01.data-model/4.rich-text.md @@ -1,10 +1,10 @@ --- stableId: 2947492c-9b05-4261-b4dd-28ba4b0ceaba title: Rich Text -description: Configure custom formats for the WYSIWYG interface, understand what the editor stores, and style the resulting HTML on your frontend. +description: Configure custom formats for the WYSIWYG interface, understand what the editor stores, and style the resulting HTML in your app's frontend CSS. --- -The [WYSIWYG interface](/guides/data-model/interfaces#wysiwyg) stores its value as HTML. This page covers the **Custom Formats** option, which adds your own formatting entries to the editor, and what the stored HTML looks like so you can style it on your frontend. +The [WYSIWYG interface](/guides/data-model/interfaces#wysiwyg) stores its value as HTML. This page covers the **Custom Formats** option, which adds your own formatting entries to the editor. ::callout{icon="i-lucide-info"} The editor is built on [Tiptap](https://tiptap.dev) as of Directus 12. It stores only the markup its schema models. See [Version 12 breaking changes](/releases/breaking-changes/version-12#wysiwyg-editor-rebuilt-on-tiptap) for the supported HTML and how existing values are normalized. @@ -25,9 +25,9 @@ Every entry needs a `title` and one of the following keys: | `selector` | One or more comma-separated block tags. Never converts the block. | | `items` | An array of entries, grouped into a submenu. See [Grouped formats](#grouped-formats). | -Each entry also takes optional `classes`, `styles`, and `attributes`. +An `inline`, `block`, or `selector` entry also needs at least one of `classes` or `attributes`, and can set both. The editor uses them to recognize the format again when the value is reloaded, so an entry carrying neither is skipped. An `items` group is a container and needs neither. -Every entry needs `classes` or `attributes` as well. The editor uses them to recognize the format again when the value is reloaded, so an entry without either is skipped. +`styles` is the only optional key. ### Example @@ -51,7 +51,13 @@ Every entry needs `classes` or `attributes` as well. The editor uses them to rec ] ``` -Applying **Lead paragraph** and **Highlight** produces: +Applying **Lead paragraph** to a paragraph, then **Highlight** to the word `lead` inside it, turns: + +```html +

A lead paragraph.

+``` + +into: ```html

A lead paragraph.

@@ -73,8 +79,16 @@ An `inline` entry renders a tag around the selection, carrying its `classes`, it } ``` +Selecting the word `text` and applying the format turns: + ```html -text +

Some text.

+``` + +into: + +```html +

Some text.

``` Inline entries are the only formats that write a `style` attribute into the content, so they are also the only ones with a visible effect inside the editor. The trade-off is an inline style in your stored HTML, which is harder to override from your frontend CSS. @@ -87,9 +101,35 @@ A `block` entry applies to one block tag and takes a single tag only. Use `selec { "title": "Lead paragraph", "block": "p", "classes": "lead" } ``` -When the selected block's tag differs from the entry's tag, the block is converted. **Conversion works between paragraphs and headings only**, because those are the two block types that hold the same content. A `block: 'h2'` entry turns a paragraph into `

`, and a `block: 'p'` entry turns a heading back into a paragraph. +When the selected block's tag differs from the entry's tag, the block is converted. **Conversion works between paragraphs and headings only**, because those are the two block types that hold the same content. + +With `{ "title": "Subheading", "block": "h2", "classes": "subheading" }`, applying the format to a paragraph turns: + +```html +

Section title

+``` + +into: + +```html +

Section title

+``` + +A `block: 'p'` entry converts the other way, turning a heading back into a paragraph. + +Every other tag applies without converting. With `{ "title": "Pull quote", "block": "blockquote", "classes": "pull-quote" }`, applying the format to a block that is already a `
` turns: + +```html +
A quote.
+``` + +into: + +```html +
A quote.
+``` -Every other tag applies without converting. A `block: 'blockquote'` entry formats blocks that are already `
` and leaves everything else alone, and logs a warning in the browser console. Use `selector` when you never want conversion. +Applying it to a paragraph leaves the paragraph untouched, and the editor logs a warning in the browser console when it loads the format. Use `selector` when you never want conversion. ### Selector formats @@ -99,7 +139,29 @@ A `selector` entry applies to the block tags you list and never changes the bloc { "title": "Subheading", "selector": "h2,h3", "classes": "subheading" } ``` -Applying this to an `

` or `

` adds the class. Applying it to a paragraph does nothing. +Applying it to an `

` or `

` adds the class, turning: + +```html +

Section title

+``` + +into: + +```html +

Section title

+``` + +Applying it to a paragraph does nothing, because `p` is not in the entry's list: + +```html +

Section title

+``` + +stays: + +```html +

Section title

+``` ### Grouped formats @@ -125,7 +187,9 @@ Groups support one level. A group nested inside another group is skipped, and a `div` and `span` are not on the list, so they cannot carry a block format. A `block: 'div'` entry is skipped. This matters when you migrate a TinyMCE configuration, since TinyMCE formats commonly wrap content in a `div`. It is the same reason a stored `
` is unwrapped when the editor loads it. Use a `section`, `article`, or `figure` entry instead, or move the wrapper into your frontend template. -For a `selector` entry, one unsupported tag skips the whole entry. Split the tags across separate entries if you want the supported ones to keep working. +A `selector` entry drops the tags the editor does not model and keeps the rest, so `selector: 'h2,div,p'` still applies to `

` and `

`. The entry is skipped only when none of its tags are modelled. + +`selector` takes plain tag names. Compound CSS selectors such as `p.lead`, `div > p`, and `#main` are skipped rather than silently matching nothing. ### Invalid entries are skipped @@ -135,9 +199,10 @@ Entries are skipped when they: - Have no `title`. - Have none of `inline`, `block`, `selector`, or `items`. -- Have no `classes` and no `attributes`. +- Have no `classes` and no `attributes`, unless the entry is an `items` group. - Use `wrapper`, which is not supported. -- Name a tag the editor does not model, such as `div`. +- Name only tags the editor does not model, such as `block: 'div'` or `selector: 'div,span'`. +- Pass a compound CSS selector such as `p.lead` or `#main` instead of a plain tag name. - Pass a comma-separated list to `block` instead of `selector`. - Nest a group inside another group. @@ -146,7 +211,7 @@ Entries are skipped when they: ### The editor does not load your frontend CSS ::callout{icon="i-lucide-triangle-alert" color="warning"} -The editor is not a preview of your site. Classes applied through custom formats have no styling attached in the Data Studio. +The editor is not a preview of your site. Classes applied through custom formats have no styling attached in the Data Studio. The exception is [inline formats](#inline-formats), which write a `style` attribute into the content and do render in the editor. :: Directus loads no project stylesheet into the editing surface, so a class like `lead`, `float-left`, or `text-center` has no visual effect inside the WYSIWYG. The class is stored and round-trips correctly. It just has nothing styling it in the Data Studio.