• Home
BuildWithMatija
Get In Touch
  1. Home
  2. Blog
  3. Payload
  4. Render Payload CMS Rich Text with Tailwind v4 — Quick Guide

Render Payload CMS Rich Text with Tailwind v4 — Quick Guide

Set up Tailwind v4's typography plugin and Payload/Lexical converters to render custom rich text blocks and images.

13th December 2025·Updated on:25th December 2025·MŽMatija Žiberna·
Payload
Render Payload CMS Rich Text with Tailwind v4 — Quick Guide

Need Help Making the Switch?

Moving to Next.js and Payload CMS? I offer advisory support on an hourly basis.

Book Hourly Advisory

Related Posts:

  • •Mastering Payload CMS API: Authentication & Queries Explained
  • •Auto-Generate Base64 Blur Placeholders in Payload CMS with Sharp
  • •When to Use Deep vs Shallow Queries in Payload CMS: A Server-Side Rendering Strategy

I was recently upgrading a project to Payload 3.0 while simultaneously adopting Tailwind CSS v4. I hit a frustrating wall: my Rich Text content, which looked great in the editor, was rendering on the frontend as a bland wall of unstyled HTML. To make matters worse, my custom image blocks were either missing or breaking the layout.

After digging through the migration docs for both libraries, I realized the issue wasn't just about applying classes—it was about how Tailwind v4 registers plugins and how Lexical handles custom React components.

This guide walks you through setting up a robust Rich Text renderer that uses Tailwind's Typography plugin for beautiful defaults while seamlessly injecting custom components like Images.

1. Configuring Tailwind v4 for Typography

The first hurdle I encountered was that my prose classes weren't doing anything. In Tailwind v3, we added plugins in tailwind.config.js. In v4, configuration has moved almost entirely to CSS.

If you skip this step, no amount of React code will fix the styling. You need to explicitly import the typography plugin in your global CSS file.

/* File: src/app/globals.css */
@import "tailwindcss";

/* This line is required to activate the 'prose' utility classes in v4 */
@plugin "@tailwindcss/typography"; 

@theme {
  /* Your other theme overrides */
}

By adding the @plugin directive, we unlock the prose classes. These classes provide expert-crafted typographic defaults (font size, line height, margins) to vanilla HTML elements, which is exactly what Payload's Rich Text outputs.

2. Creating the Custom Block Component

Before we build the renderer, we need the component we want to render. I wanted to insert images directly into the text flow but needed control over alignment, width, and rounded corners.

Here is the RichTextImage component. Note the use of the not-prose class—this is critical. Without it, the parent prose styles will target your figure and img tags, overriding your custom padding and margins with generic blog styles.

// File: src/components/blocks/richtext-image/richtext-image.tsx
'use client';

import { PayloadImageClient } from '@/components/payload/images/payload-image-client';
import { Media } from '@payload-types';
import { cn } from '@/lib/utils';
import { isObject } from '@/lib/type-guards';

export interface RichTextImageProps {
  image: Media | number | null | undefined;
  caption?: string;
  width?: 'full' | 'large' | 'medium' | 'small';
  alignment?: 'left' | 'center' | 'right';
  rounded?: boolean;
  className?: string;
}

export function RichTextImage({
  image,
  caption,
  width = 'large',
  alignment = 'center',
  rounded = true,
  className,
}: RichTextImageProps) {
  // Ensure we don't crash if the image relation is broken
  if (!isObject<Media>(image)) {
    return null;
  }

  const widthClasses = {
    full: 'w-full',
    large: 'w-full lg:w-4/5',
    medium: 'w-full lg:w-3/5',
    small: 'w-full lg:w-2/5',
  };

  const alignmentClasses = {
    left: 'mr-auto',
    center: 'mx-auto',
    right: 'ml-auto',
  };

  return (
    <figure
      className={cn(
        // 'not-prose' protects this component from the parent typography styles
        'not-prose my-8 flex flex-col gap-4',
        widthClasses[width],
        alignmentClasses[alignment],
        className,
      )}
    >
      <div className={cn('relative w-full aspect-auto', rounded && 'rounded-lg overflow-hidden bg-neutral-100')}>
        <PayloadImageClient
          image={image}
          context="full" // Adjust based on your image component logic
          fill={false}
          className="w-full h-auto"
        />
      </div>

      {caption && <figcaption className="text-sm text-muted-foreground text-center">{caption}</figcaption>}
    </figure>
  );
}

This component accepts props directly from Payload's JSON structure. By handling the not-prose class here, we ensure that our layout choices (like w-4/5) are respected.

3. Building the RichText Renderer

Now we need the component that takes the raw JSON from Payload and transforms it into React components. Payload provides a RichText component, but we need to feed it a custom converters function to tell it how to handle our richtext_image block.

// File: src/components/rich-text-renderer.tsx
'use client';

import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical';
import { RichText as ConvertRichText, JSXConvertersFunction } from '@payloadcms/richtext-lexical/react';
import { RichTextImage } from '@/components/blocks/richtext-image/richtext-image';
import { cn } from '@/lib/utils';

type Props = {
  data: DefaultTypedEditorState;
  enableGutter?: boolean;
  enableProse?: boolean;
} & React.HTMLAttributes<HTMLDivElement>;

const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => {
  return {
    ...defaultConverters,
    // We override the 'blocks' handling to catch our specific image block
    blocks: {
      ...defaultConverters?.blocks,
      richtext_image: ({ node }: { node: any }) => (
        <RichTextImage
          image={node.fields.image}
          caption={node.fields.caption}
          width={node.fields.width}
          alignment={node.fields.alignment}
          rounded={node.fields.rounded}
        />
      ),
    },
  };
};

export function RichTextRenderer({
  data,
  className,
  enableProse = true,
  enableGutter = true,
  ...rest
}: Props) {
  if (!data) {
    return null;
  }

  return (
    <ConvertRichText
      data={data}
      converters={jsxConverters}
      className={cn(
        'payload-richtext break-words',
        {
          'container mx-auto': enableGutter,
          'max-w-none': !enableGutter,
          // The magic happens here: apply prose classes conditionally
          'prose prose-sm md:prose-base lg:prose-lg dark:prose-invert': enableProse,
        },
        className,
      )}
      {...rest}
    />
  );
}

In the jsxConverters function, we match the key richtext_image (which corresponds to the block slug in your Payload config) to our React component. We effectively swap the raw JSON node for our rendered RichTextImage.

4. Registering the Block in Payload Config

Finally, for this to work, the block must exist in your Payload Schema. This ensures the Content Editor sees the "Rich Text Image" option within the Slash menu of the editor.

// File: src/payload.config.ts (or wherever your config lives)
import { buildConfig } from 'payload/config';
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical';
import { RichTextImageBlock } from '@/blocks/richtext-image/schema'; // Your block schema definition

export default buildConfig({
  // ... other config settings
  editor: lexicalEditor({
    features: ({ defaultFeatures }) => [
      ...defaultFeatures,
      // Enable the blocks feature and register our custom block
      BlocksFeature({
        blocks: [RichTextImageBlock],
      }),
    ],
  }),
});

The BlocksFeature is what connects your schema definition to the Lexical editor. Once registered, Payload will store the data with the richtext_image slug, which our Renderer listens for on the frontend.

Conclusion

Rendering Rich Text doesn't have to be a choice between "ugly HTML" or "writing a parser from scratch." By combining Tailwind v4's typography plugin with Payload's Lexical converters, we get the best of both worlds: nice default typography for standard text, and full React control for complex blocks.

You now have a renderer you can drop into any page, knowing your images will look sharp and your text will be legible.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija

📚 Comprehensive Payload CMS Guides

Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.

No spam. Unsubscribe anytime.

📄View markdown version
4

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in

Mastering Payload CMS API: Authentication & Queries Explained
Mastering Payload CMS API: Authentication & Queries Explained

5th November 2025

Auto-Generate Base64 Blur Placeholders in Payload CMS with Sharp
Auto-Generate Base64 Blur Placeholders in Payload CMS with Sharp

6th October 2025

When to Use Deep vs Shallow Queries in Payload CMS: A Server-Side Rendering Strategy
When to Use Deep vs Shallow Queries in Payload CMS: A Server-Side Rendering Strategy

24th September 2025

Table of Contents

  • 1. Configuring Tailwind v4 for Typography
  • 2. Creating the Custom Block Component
  • 3. Building the RichText Renderer
  • 4. Registering the Block in Payload Config
  • Conclusion
On this page:
  • 1. Configuring Tailwind v4 for Typography
  • 2. Creating the Custom Block Component
  • 3. Building the RichText Renderer
  • 4. Registering the Block in Payload Config
  • Conclusion
Build With Matija Logo

Build with Matija

Matija Žiberna

I turn scattered business knowledge into one usable system. End-to-end system architecture, AI integration, and development.

Quick Links

Payload CMS Websites
  • Bespoke AI Applications
  • Projects
  • How I Work
  • Blog
  • Get in Touch

    Have a project in mind? Let's discuss how we can help your business grow.

    Contact me →
    © 2026BuildWithMatija•Principal-led system architecture•All rights reserved