---
title: @storyblok/richtext (Version 4.x)
description: @storyblok/richtext is a custom resolver for Storyblok rich text in JavaScript applications.
url: https://storyblok.com/docs/libraries/js/rich-text
---

# @storyblok/richtext (Version 4.x)

`@storyblok/richtext` is a custom resolver for Storyblok rich text in JavaScript applications.

> [!NOTE]
> In version 4.x, both parsing (HTML/Markdown to JSON) and rendering (JSON to HTML) are now powered by Tiptap extensions. Each extension defines `parseHTML` and `renderHTML` in a single place, replacing the separate resolver map from version 3.x. Read the [announcement](/mp/introducing-storyblok-richtext-v4) for further details and upgrade instructions.

## Installation

Add the package to a project by running this command in the terminal:

```bash
npm install @storyblok/richtext@latest
```

## Usage

### Basic

```js
import { richTextResolver } from "@storyblok/richtext";
const { render } = richTextResolver();
const html = render(doc);
document.querySelector("#app").innerHTML = `<div>${html}</div>`;
```

### Custom Tiptap extensions

The recommended way to customize rendering is via the `tiptapExtensions` option. Extensions follow the [Tiptap extension API](https://tiptap.dev/docs/editor/extensions/custom-extensions) and work for both parsing and rendering.

#### Add a custom node

Create a custom node as demonstrated in the example below:

```ts
import { Node } from "@tiptap/core";
import { richTextResolver } from "@storyblok/richtext";

const Callout = Node.create({
  name: "callout",
  group: "block",
  content: "inline*",
  parseHTML() {
    return [{ tag: "div[data-callout]" }];
  },
  renderHTML({ HTMLAttributes }) {
    return ["div", { "data-callout": "", class: "callout", ...HTMLAttributes }, 0];
  },
});

const { render } = richTextResolver({
  tiptapExtensions: { callout: Callout },
});

const html = render(doc);
```

Use the same extension to parse HTML via `htmlToStoryblokRichtext`:

```ts
import { htmlToStoryblokRichtext } from "@storyblok/richtext/html-parser";

const json = htmlToStoryblokRichtext(html, {
  tiptapExtensions: { callout: Callout },
});
```

#### Override a built-in extension

Pass an extension with the same key as a built-in to replace it. Use `.extend()` on a Tiptap extension to inherit its `parseHTML` logic and only override `renderHTML`:

```ts
import Heading from "@tiptap/extension-heading";
import { richTextResolver } from "@storyblok/richtext";

const CustomHeading = Heading.extend({
  renderHTML({ node, HTMLAttributes }) {
    const level = node.attrs.level;
    return [`h${level}`, { class: `heading-${level}`, ...HTMLAttributes }, 0];
  },
});

const { render } = richTextResolver({
  tiptapExtensions: { heading: CustomHeading },
});
```

### Framework components with `asTag`

Tiptap’s `renderHTML` returns a `DOMOutputSpec`, a nested array where the first element is a string tag name like `'a'` or `'div'`. In framework SDKs, it is usually preferable to render a framework component instead.

The `asTag` helper casts a component reference to support TypeScript while the rich text renderer handles it correctly at runtime:

```ts
import { Mark } from "@tiptap/core";
import { asTag } from "@storyblok/vue"; // also available from @storyblok/react, @storyblok/richtext
import { RouterLink } from "vue-router";

const CustomLink = Mark.create({
  name: "link",
  renderHTML({ HTMLAttributes }) {
    if (HTMLAttributes.linktype === "story") {
      return [asTag(RouterLink), { to: HTMLAttributes.href }, 0];
    }
    return ["a", { href: HTMLAttributes.href, target: HTMLAttributes.target }, 0];
  },
});

const { render } = richTextResolver({
  tiptapExtensions: { link: CustomLink },
});
```

> [!TIP]
> When used with Vue’s `h` or React’s `createElement` as the `renderFn`, the component reference is passed directly to the framework’s element factory, producing framework component instances.

### Blok component rendering

Storyblok components are embedded in rich text as `blok` nodes. Each such node contains a `body` array with one or more components that need to be rendered as actual framework components, not as HTML tags.

`@storyblok/richtext` version 4.x handles this through `ComponentBlok.configure({ renderComponent })`, a callback on the blok extension that receives each blok and returns framework-native output. This is something usually done, if needed, at frontend framework level.

> [!TIP]
> If you use a framework SDK (`@storyblok/vue`, `@storyblok/react`, `@storyblok/nuxt`), blok rendering works automatically. You only need to configure `renderComponent` when using `@storyblok/richtext` directly or overriding the default behavior.

### Optimize images

To optimize `images`, use the `optimizeImages` property on the `richTextResolver` options. For a full list of available options, refer to the [Image Service documentation](/docs/api/image-service/).

```js
import { richTextResolver } from "@storyblok/richtext";

const html = richTextResolver({
  optimizeImages: {
    class: "my-peformant-image",
    loading: "lazy",
    width: 800,
    height: 600,
    srcset: [400, 800, 1200, 1600],
    sizes: ["(max-width: 400px) 100vw", "50vw"],
    filters: {
      format: "webp",
      quality: 10,
      grayscale: true,
      blur: 10,
      brightness: 10,
    },
  },
}).render(doc);
```

## Markdown to rich text

The package includes a utility for converting Markdown content to Storyblok’s rich text format, which can be rendered via `richTextResolver`.

Supported markdown elements:

-   Text formatting: `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``, `[links](url)`
-   Headings: `# H1` through `###### H6`
-   Lists: `- unordered` and `1. ordered lists` with nesting
-   Code blocks: ` ```fenced``` ` and indented blocks
-   Blockquotes: `> quoted text`
-   Images: `![alt](src "title")`
-   Links: `[text](url)` and `[text](url "title")`
-   Tables: Standard markdown table syntax
-   Horizontal rules: `---`
-   Line breaks: (two spaces) for hard breaks

```js
import { markdownToStoryblokRichtext } from "@storyblok/richtext/markdown-parser";

const markdown = `
# Main Heading

This is a **bold** paragraph with *italic* text.

- List item 1
- List item 2

> This is a blockquote
`;

const richtextDoc = markdownToStoryblokRichtext(markdown);

const html = richTextResolver().render(richtextDoc);
document.getElementById("content").innerHTML = html;
```

Customize how specific Markdown elements are parsed by providing custom Tiptap extensions:

```js
import { markdownToStoryblokRichtext } from "@storyblok/richtext/markdown-parser";

const richtextDoc = markdownToStoryblokRichtext(markdown, {
  tiptapExtensions: { heading: CustomHeading },
});
```

## HTML to Storyblok rich text

The package includes a utility for converting HTML content to Storyblok’s rich text format, which can be rendered via `richTextResolver`.

Supported HTML elements:

-   Headlines: `<h1-6>`
-   Paragraphs: `<p>`
-   Lists: `<ol>`, `<ul>`, `<li>`
-   Tables: `<table>`, `<thead>`, `<tbody>` , `<tr>`, `<th>`, `<td>`
-   Blockquote: `<blockquote>`
-   Code: `<pre>`, `<code>`
-   Links: `<a>`
-   Formatting: `<strong>`, `<b>`, `<em>`, `<i>`, `<del>`, `<s>`
-   Images: `<img>`
-   Misc: `<span>`, `<hr>`, `<br>`

```js
import { htmlToStoryblokRichtext } from "@storyblok/richtext/html-parser";

const html = `
<h1>Main Heading</h1>

<p>This is a <strong>bold</strong> paragraph with <em>italic</em> text.</p>

<ul>
 <li>List item 1</li>
 <li>List item 2</li>
</ul>

<blockquote>
 <p>This is a blockquote</p>
</blockquote>
`;

const richtextDoc = htmlToStoryblokRichtext(html);

const output = richTextResolver().render(richtextDoc);
document.getElementById("content").innerHTML = output;
```

Customize parsing by providing custom tiptap extensions:

```ts
import { Node } from "@tiptap/core";
import { htmlToStoryblokRichtext } from "@storyblok/richtext/html-parser";

const Callout = Node.create({
  name: "callout",
  group: "block",
  content: "inline*",
  parseHTML() {
    return [{ tag: "div[data-callout]" }];
  },
  renderHTML({ HTMLAttributes }) {
    return ["div", { "data-callout": "", class: "callout", ...HTMLAttributes }, 0];
  },
});

const richtextDoc = htmlToStoryblokRichtext(html, {
  tiptapExtensions: { callout: Callout },
});
```

## Framework usage

The `@storyblok/richtext` package is framework-agnostic and can be used with any JavaScript-based frontend framework. Below are examples of how to use the package with different frameworks.

### React

> [!NOTE]
> For a better developer experience, use the corresponding APIs available in the framework SDK, such as the `StoryblokRichText` component in `@storyblok/react`. Learn more in the [@storyblok/react reference](/docs/libraries/js/react-sdk).

```tsx
import React from "react";
import { richTextResolver } from "@storyblok/richtext";

const options: StoryblokRichTextOptions<ReactElement> = {
  renderFn: React.createElement,
  keyedResolvers: true,
};

function Example({ doc }) {
  const html = richTextResolver(options).render(doc);
  return <>{formattedHtml}</>;
}
```

Refer to [playground/react](https://github.com/storyblok/richtext/blob/main/playground/react) in the `@storyblok/richtext` package repository for a complete example.

### Vue

> [!NOTE]
> For a better developer experience, use the corresponding APIs available in the framework SDK, such as the `StoryblokRichText` component in `@storyblok/vue`. Learn more in the [@storyblok/vue reference](/docs/libraries/js/vue-sdk).

```vue-html
<script setup>
  import type { VNode } from 'vue';
  import { createTextVNode, h } from 'vue';
  import { richTextResolver, type StoryblokRichTextOptions } from '@storyblok/richtext';

  const options: StoryblokRichTextOptions<VNode> = {
    renderFn: h,
    textFn: createTextVNode,
    keyedResolvers: true,
  };

  const root = () => richTextResolver<VNode>(options).render(doc);
</script>

<template>
  <root />
</template>
```

Refer to [playground/vue](https://github.com/storyblok/richtext/blob/main/playground/vue) in the `@storyblok/richtext` package repository for a complete example.

## TypeScript generics

Correct type support in a framework-agnostic way is ensured by using [Typescript generics](https://www.typescriptlang.org/docs/handbook/2/generics.html), circumventing the need to import types and require framework packages as dependencies.

### Vanilla: `string`

```ts
import { Mark } from "@tiptap/core";
import { richTextResolver } from "@storyblok/richtext";

const CustomLink = Mark.create({
  name: "link",
  renderHTML({ HTMLAttributes }) {
    return ["a", { href: HTMLAttributes.href, class: "custom-link" }, 0];
  },
});

const html = richTextResolver<string>({
  tiptapExtensions: { link: CustomLink },
}).render(doc);
```

### React: `React.ReactElement`

```ts
const options: StoryblokRichTextOptions<React.ReactElement> = {
  renderFn: React.createElement,
  keyedResolvers: true,
};
const root = () => richTextResolver<React.ReactElement>(options).render(doc);
```

### Vue: `VNode`

```ts
const options: StoryblokRichTextOptions<VNode> = {
  renderFn: h,
  keyedResolvers: true,
};
const root = () => richTextResolver<VNode>(options).render(doc);
```

## Further resources

[@storyblok/richtext HTML Sanitization Tutorial](https://storyblok.com/tp/rich-text-html-sanitization) Read the tutorial to learn how to sanitize the HTML string output of the rich text resolver.

## Previous versions

[@storyblok/richtext (Version 3.x)](/docs/libraries/js/rich-text/v3)

## Pagination

-   [Previous: Preview Bridge](/docs/libraries/js/preview-bridge)
-   [Next: Migration Utilities](/docs/libraries/js/migrations)
