Updated on 2025-02-24T05:50:16.632Z
Written on 2025-02-21T08:41:10.072Z
Everybody loves Mermaid.js. If you don't know what it is then go check it out at: https://mermaid.js.org/. It's the easiest way (next to copy-paste an image file) to put diagrams of all kinds on your HTML page with the simplest syntax on this planet.
Below showcases what each type of diagram looks like:
Loading diagram...
Loading diagram...
Loading diagram...
Enough with the showcases, let's see how it's done!
First, create a new Payload project with npx create-payload-app
or integrate Payload CMS into your existing Next.js application (check out details here). Once your Payload project is ready, install the Mermaid.js library with good old npm i mermaid
. I won't go into details on how to create collections and add an editor. You can check them all out at Payload's official documentation.
To render Mermaid diagrams, you’ll need a dedicated client-side component. This component will parse Mermaid syntax and initialize the diagrams. Since Mermaid requires access to the DOM, this component must run only on the client side.
Mermaid will not re-render if the syntax has changed after the component is loaded. I've been struggling for real-time render for a while but thanks to this article, I finally was able to make it work.
NOTE: if you don't want the preview to consume too much resource on rendering, you can just add a refresh button and use onClick
to remove the data-processed
attribute and run mermaid.contentLoaded()
instead.
This component will serve two purposes:
Now you have got the diagram renderer but there's no way to input the data into it yet. Let's create an input component so that you can create/edit it in Lexical
Payload’s Lexical editor supports custom blocks, allowing you to add a dedicated “Mermaid Diagram” block to your rich text fields. Configure this block to store raw Mermaid syntax and reference your client component for previewing. Let's create a configuration that imports the input component that we just created so that you can later load it as a custom block for Lexical.
Then on the collection file, import the block above and put it in the blocks
array under BlocksFeature
.
When rendering whatever mutates on the client side in a Next.js, you’ll likely encounter the hydration error. This will happen to because Mermaid will only render the diagram after the component is loaded to the browser. To fix this you can either try any of these:
suppressHydrationWarning={true}
to the parent element that will render the diagram. In this case it should be the <div>
element with mermaid
class in mermaid-renderer.tsx
orFor 2, you can just create another .tsx
file with the sample code below. One thing to note when creating a dynamic component is that the path
here must be absolute, and the target file to import the component must export it as default (see further).
This ensures the diagram is generated after the page loads, eliminating conflicts between serverrendered HTML and client-side DOM manipulation.
In your frontend, configure the RichText
element to recognize the custom Mermaid block. Create Payload’s jsxConverters
property to link the block to your dynamic client component. Then pass the jsxConverters
to the RichText
component on your page. This tells frontend to replace the Mermaid block with your dynamically loaded component.
You’ve now empowered your Payload CMS to handle Mermaid.js diagrams end-to-end. Content editors can create and preview visuals effortlessly, while your website delivers interactive, client-rendered diagrams without hydration issues.
This setup isn’t just for blogs—use it for documentation, project roadmaps, or data dashboards. Mermaid’s flexibility combined with Payload’s headless architecture opens endless possibilities for dynamic content.
TIP: Explore Mermaid’s theming options to match your site’s design, or add interactivity with click events. The sky’s the limit!
Now go forth and diagram all the things. 🌊
// ./components/payload/mermaid-renderer.tsx
"use client";
import React from "react";
import mermaid from "mermaid";
import { useEffect, useState, useRef } from "react";
interface Mermaid {
data: string;
}
export default function Mermaid({ data }: Mermaid) {
const diagramRef = useRef<HTMLDivElement>(null);
useEffect(() => {
mermaid.initialize({
startOnLoad: true,
});
}, []);
// Rerender when data changes
useEffect(() => {
// Remove this attribute to make it re-render
diagramRef.current!.removeAttribute("data-processed");
diagramRef.current!.innerHTML = data;
mermaid.contentLoaded();
}, [data]);
return (
<>
<div
className="mermaid"
ref={diagramRef}
/>
</>
);
}
// ./components/payload/mermaid/index.tsx
"use client";
import type { TextareaFieldClient, TextareaFieldClientProps } from "payload";
import Mermaid from "@/components/payload/mermaid-renderer";
import { useFormFields, TextareaField } from "@payloadcms/ui";
import React, { useMemo } from "react";
export const MermaidBlockInput: React.FC<TextareaFieldClientProps> = ({
field,
path,
permissions,
readOnly,
renderedBlocks,
schemaPath,
validate,
}) => {
const mermaidCodeField = useFormFields(([fields]) => fields["code"]);
const label = [];
const props: TextareaFieldClient = useMemo<TextareaFieldClient>(
() => ({
...field,
type: "textarea",
admin: {
...field.admin,
label,
},
}),
[field],
);
const key = `${field.name}`;
return (
<>
// Input area
<TextareaField
field={props}
forceRender={true}
key={key}
path={path}
permissions={permissions}
readOnly={readOnly}
renderedBlocks={renderedBlocks}
schemaPath={schemaPath}
validate={validate}
/>
// Preview area
<div>
<p>Preview</p>
<Mermaid data={mermaidCodeField?.value as string} />
</div>
</>
);
};
// @/components/payload/dynamic-mermaid-renderer
"use client";
import dynamic from "next/dynamic";
const Mermaid = dynamic(() => import("./mermaid-renderer"), {
ssr: false,
loading: () => <p>Loading diagram...</p>,
});
export const DynamicMermaid = ({ data }) => <Mermaid data={data} />;
import { DynamicMermaid } from "@/components/payload/mermaid/dynamic-mermaid-renderer";
import {
type JSXConvertersFunction,
RichText,
} from "@payloadcms/richtext-lexical/react";
const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
...defaultConverters,
blocks: {
Mermaid: ({ node }) => (
<>
<DynamicMermaid data={node.fields.code} />
</>
),
},
});
export default function Blog() {
// get your richtext content here
let richtextContent
return (
<>
<RichText
// attach the converter here
converters={jsxConverters}
data={richtextContent}
/>
</>
)
}
// ./lib/payload/mermaid-block.ts
import type { Block } from 'payload';
export const mermaidBlock: Block = {
slug: 'Mermaid',
fields: [
{
admin: {
components: {
Field: '/components/payload/mermaid#MermaidBlockInput',
},
},
name: 'code',
type: 'textarea',
},
],
}
import type { CollectionConfig } from 'payload'
import {
lexicalEditor,
BlocksFeature,
} from '@payloadcms/richtext-lexical'
import { mermaidBlock } from '@/lib/payload/blocks-feature/mermaid-block'
export const Blog: CollectionConfig = {
slug: 'blog',
access: {
read: () => true,
},
fields: [
{
name: 'content',
type: 'richText',
required: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures,
BlocksFeature({
blocks: [mermaidBlock] // insert it here
}),],
}),
}
],
}