---
title: "Render Payload CMS Rich Text with Tailwind v4 — Quick Guide"
slug: "render-payload-cms-rich-text-tailwind-v4"
published: "2025-12-13"
updated: "2025-12-25"
categories:
  - "Payload"
tags:
  - "Payload CMS rich text"
  - "Tailwind v4 typography"
  - "Lexical converters"
  - "Payload RichText renderer"
  - "RichTextImage"
  - "not-prose"
  - "PayloadImageClient"
  - "custom rich text blocks"
  - "Tailwind typography plugin"
  - "render rich text"
llm-intent: "how-to"
audience-level: "intermediate"
llm-purpose: "Payload CMS rich text rendered with Tailwind v4 — step-by-step guide to enable the typography plugin, use Lexical converters, and add custom image…"
llm-prereqs:
  - "Payload CMS"
  - "Tailwind CSS v4"
  - "@tailwindcss/typography"
  - "Lexical"
  - "React"
  - "TypeScript"
  - "@payloadcms/richtext-lexical"
  - "PayloadImageClient"
---

**Summary Triples**
- (Render Payload CMS Rich Text with Tailwind v4 — Quick Guide, expresses-intent, how-to)
- (Render Payload CMS Rich Text with Tailwind v4 — Quick Guide, covers-topic, Payload CMS rich text)
- (Render Payload CMS Rich Text with Tailwind v4 — Quick Guide, provides-guidance-for, Payload CMS rich text rendered with Tailwind v4 — step-by-step guide to enable the typography plugin, use Lexical converters, and add custom image…)

### {GOAL}
Payload CMS rich text rendered with Tailwind v4 — step-by-step guide to enable the typography plugin, use Lexical converters, and add custom image…

### {PREREQS}
- Payload CMS
- Tailwind CSS v4
- @tailwindcss/typography
- Lexical
- React
- TypeScript
- @payloadcms/richtext-lexical
- PayloadImageClient

### {STEPS}
1. Enable Tailwind typography plugin
2. Create RichTextImage component
3. Implement RichText renderer with converters
4. Register block in Payload config
5. Apply prose and render on pages

<!-- llm:goal="Payload CMS rich text rendered with Tailwind v4 — step-by-step guide to enable the typography plugin, use Lexical converters, and add custom image…" -->
<!-- llm:prereq="Payload CMS" -->
<!-- llm:prereq="Tailwind CSS v4" -->
<!-- llm:prereq="@tailwindcss/typography" -->
<!-- llm:prereq="Lexical" -->
<!-- llm:prereq="React" -->
<!-- llm:prereq="TypeScript" -->
<!-- llm:prereq="@payloadcms/richtext-lexical" -->
<!-- llm:prereq="PayloadImageClient" -->

# Render Payload CMS Rich Text with Tailwind v4 — Quick Guide
> Payload CMS rich text rendered with Tailwind v4 — step-by-step guide to enable the typography plugin, use Lexical converters, and add custom image…
Matija Žiberna · 2025-12-13

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.

```css
/* 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.

```tsx
// 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.

```tsx
// 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.

```typescript
// 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