Rich Text Editor Styling with Tailwind and TipTap: Getting the prose Class Right
AI generated
tw
Tailwind CSS · TipTap · Rich Text
Rich Text Editor Styling with TipTap
Getting the prose class, toolbar states, and the editor-versus-render gap right

TipTap builds on ProseMirror and deliberately ships without any visual styling of its own, only the editable structure and the commands for formatting. The entire look, from the running text inside the editor to the toolbar buttons to the final rendering of saved content, comes exclusively from custom CSS. The @tailwindcss/typography prose class provides a solid starting point for that, but it needs targeted adjustments so the editor view and the render view genuinely look consistent.

16 min read prose · @tailwindcss/typography TipTap · ProseMirror

1. TipTap and ProseMirror: why the library ships without its own styles

TipTap is a toolkit built around the ProseMirror editor core and provides a declarative, extensible API for formatting commands, extensions, and schema definitions, without prescribing a single visual design. That deliberate choice makes it possible to use the same editor across completely different design systems, from a minimalist comment field to a full-fledged document editor, without TipTap itself ever needing adjustment for that.

At its core, a TipTap editor's editable area is a contenteditable element managed by ProseMirror, but it remains visually completely unstyled until custom CSS is applied. Without that customization, headings, lists, and quotes look exactly like unformatted running text inside the editor, even though the underlying document structure is already correct, which makes the need for a deliberate styling strategy obvious from the start.

2. Adapting the prose class for the editable area

The prose class from the @tailwindcss/typography plugin provides predefined styles for typographic elements like headings, paragraphs, lists, and quotes, originally intended for static, Markdown-rendered content. Applied to the TipTap editor, usually through the editorProps.attributes.class configuration, the editable area immediately gets consistent base typography, without having to style every single HTML element manually.

The editor context still needs additional adjustments compared to pure static prose content, say a visible cursor with sufficient contrast, a clearly recognizable focus marker for the entire editable area, and enough padding so text does not sit flush against the edge of the editor container. Those adjustments are usually added through extra utility classes alongside prose, rather than by modifying the typography plugin configuration itself.


const editor = useEditor({
  extensions: [StarterKit, Placeholder.configure({
    placeholder: "Start writing ...",
  })],
  editorProps: {
    attributes: {
      class:
        "prose prose-slate max-w-none focus:outline-none " +
        "min-h-[240px] px-4 py-3 rounded-lg border border-slate-200",
    },
  },
});

3. Difference between the editor view and the final render view

The editable area and the later, read-only rendering of the same content share the same underlying HTML structure, but they need different interaction styles. Inside the editor, cursor visibility, focus rings, and possibly placeholder text matter, while the render view should drop these interaction cues entirely, since no editing happens there and such cues would only confuse readers.

In practice, it works well to use the same prose base class in both contexts, but add editor-specific rules only in the editor context through an extra modifier class like is-editor. That way, the underlying typography, heading sizes, line spacing, list formatting, is guaranteed to stay identical between both views, while only interaction-related details like the focus ring and cursor color vary between editor and render view.

4. Styling toolbar buttons with active and inactive states

TipTap's editor.isActive('bold') method returns a boolean state that is true exactly when the current cursor position or text selection already carries that formatting. That state is the foundation for visual toolbar feedback: an active bold button should clearly differ from an inactive one, usually through a darker background or a colored border, so users can always tell which formatting is already active at the current cursor position.

It matters that the formatting command itself runs independently of the visual state, say via editor.chain().focus().toggleBold().run(), while the button's class assignment reacts purely declaratively to isActive. That separation ensures the visual state always stays in sync with the actual editor state, even when formatting changes through a keyboard shortcut instead of a toolbar click.


function ToolbarButton({ editor, format, label }) {
  const isActive = editor.isActive(format);
  return (
    <button
      type="button"
      onClick={() => editor.chain().focus().toggleBold().run()}
      className={`rounded px-2.5 py-1.5 text-sm font-medium transition-colors ${
        isActive
          ? "bg-slate-800 text-white"
          : "text-slate-600 hover:bg-slate-100"
      }`}
      aria-pressed={isActive}
    >
      {label}
    </button>
  );
}

5. Placeholder text styling with the Placeholder extension

TipTap's official Placeholder extension inserts placeholder text through a CSS pseudo-element, specifically ::before on the first empty paragraph, whenever the editor holds no content. Unlike a classic input element with a native placeholder attribute, this behavior has to be explicitly recreated through CSS for a contenteditable-based editor, because the browser provides no built-in placeholder mechanism for arbitrarily structured, editable content.

For a consistent appearance, the placeholder text should use the same font size and line spacing as the actual running text, but clearly identify itself as a placeholder through a lighter text color, similar to native form fields. A common styling mistake is too strong a contrast gap between placeholder and real text, which produces an irritating, visible color jump as soon as the user starts typing.


/* Placeholder styling for TipTap's Placeholder extension */
.tiptap p.is-editor-empty:first-child::before {
  content: attr(data-placeholder);
  color: theme(colors.slate.400);
  float: left;
  height: 0;
  pointer-events: none;
}

6. Custom prose overrides for editor-specific elements

The default prose styles cover classic text elements well, but TipTap editors often contain additional, editor-specific elements like embedded mentions, code blocks with syntax highlighting, or interactive task lists with checkboxes, for which the default prose class ships no sensible defaults. Those elements need their own, extra CSS rules, targeted through the prose-* modifier utilities, say prose-code:bg-slate-100 for inline code or prose-a:text-sky-600 for links.

With task lists using checkboxes, it particularly matters that the browser's native checkbox rendering does not collide with the prose list styles, which in practice usually means taking the list item itself out of the normal list-style flow and placing the checkbox instead through its own flex layout next to the text content. This kind of special-casing for individual element types is the main reason plain default prose configuration rarely stays fully sufficient for more complex TipTap editors.

7. Practical example: a complete editor component with toolbar and status bar

A complete editor component combines the customized prose class for the content area, a toolbar with clearly recognizable active states for formatting commands, and usually a slim status bar showing word count or save status. The toolbar should be visually clearly separated from the actual editing area, typically through a subtle divider line or a slightly different background tone, so users can immediately distinguish controls from content.

For responsive display, it is worth making the toolbar horizontally scrollable on narrow screens instead of letting buttons wrap, because wrapped toolbar rows eat into valuable vertical space needed for the actual editing area. A horizontally scrollable toolbar with a subtly styled scrollbar, the kind that also suits other UI areas, keeps the editor component compact and functional even on small screens.

8. Dark mode: prose-invert and the editor cursor color

The typography plugin ships prose-invert as a ready-made dark mode variant that automatically adjusts text colors, headings, and list markers for dark backgrounds once activated together with a dark: variant. That adjustment covers only the plain prose content, though, and not automatically the editor-specific extra styles, which is why cursor color, focus ring, and placeholder color need to be checked separately in dark mode and adjusted where necessary.

A particularly easy point to overlook is the text selection color, which the browser renders by default through ::selection and which usually offers sufficient contrast in light mode, but quickly becomes hard to read in dark mode. An explicit ::selection rule with an adjusted background color for the dark mode context ensures selected text stays legible even against a dark editor background.

9. Common pitfalls: preflight, ProseMirror base classes, and the focus ring

Tailwind's preflight reset removes browser default styles for headings, lists, and quotes by default, which combined with ProseMirror's own base classes can lead to duplicated or conflicting rules whenever both systems try to style the same elements. In practice it is best to rely consistently on the prose class as the single source of typographic base rules and not additionally override ProseMirror's own classes manually, to avoid specificity conflicts.

Another common mistake is a missing or too-subtle focus ring on the entire editable area, leaving users, particularly keyboard users, unsure whether the editor actually holds input focus. Instead of setting focus:outline-none without a replacement, an alternative, clearly visible focus marker should always be added, say a colored border via focus-within:ring-2 on the enclosing editor container.

Area Responsible class/API Typical adjustment Important note
Running text in editor prose (typography plugin) prose-slate, max-w-none, padding Base for both editor and render view
Active toolbar button editor.isActive('format') bg-slate-800, text-white when active Set aria-pressed for accessibility
Placeholder Placeholder extension + ::before color: theme(colors.slate.400) Same font size as real text
Task lists/code blocks prose-code:, prose-a: modifiers Custom color and layout rules Default prose does not cover these elements
Focus state focus-within: on editor container ring-2, ring-sky-500 Never focus:outline-none without a replacement

Mironsoft

Tailwind CSS architecture, design systems, and performance

Tailwind frontends that stay maintainable despite thousands of utility classes?

We review existing Tailwind projects for bloated class lists, inconsistent design tokens, and unused CSS remnants, then build a design system that scales cleanly instead of getting messier with every component.

Design System Review

Checking tokens, spacing scale, and component consistency for maintainability.

Performance Optimization

Systematically reducing CSS bundle size, purge configuration, and load times.

Component Architecture

Building reusable, well-structured components instead of sprawling class lists.

10. Summary

Rich Text Editor Styling with TipTap: The Essentials at a Glance

Core idea

TipTap ships no styles of its own, the prose class from @tailwindcss/typography provides the typographic base.

Editor vs. render

Both views share the same prose base, editor-specific rules get added through an extra modifier class.

Toolbar state

editor.isActive('format') returns the boolean state for visual feedback, independent of the actual formatting command.

Most common pitfalls

Preflight/ProseMirror conflicts, missing focus ring after focus:outline-none, too strong placeholder contrast.

11. FAQ: Rich Text Editor Styling with TipTap: The Essentials at a Glance

1Why does my TipTap editor look completely unstyled?
TipTap deliberately ships without its own visual styles. Without a CSS class like prose from the @tailwindcss/typography plugin, headings, lists, and quotes stay visually identical to plain running text.
2How do I enable the prose class in the TipTap editor?
Through the editorProps.attributes.class configuration when creating the editor, where prose is specified together with other utility classes like max-w-none and padding.
3Do I need to style the editor view and the render view differently?
The typographic base should stay identical. Only editor-specific details like cursor color, focus ring, and placeholder text need extra rules that apply only in the editor context.
4How do I know whether a toolbar button should currently be active?
With editor.isActive('format'), for example editor.isActive('bold'), which returns a boolean state that flows into a conditional class list for the button.
5How does placeholder text work in a contenteditable-based editor?
Through TipTap's Placeholder extension, which inserts a ::before pseudo-element on the first empty paragraph. Unlike input fields, there is no native placeholder mechanism for contenteditable.
6Does the default prose class cover all TipTap elements?
No, editor-specific elements like task lists with checkboxes or code blocks with syntax highlighting need their own extra rules, usually through prose-* modifier utilities.
7How do I enable dark mode for the editor?
With prose-invert from the typography plugin for the plain text content. Cursor color, focus ring, and text selection color need separate manual adjustment for dark mode.
8Why shouldn't I set focus:outline-none without a replacement?
Without a visible focus state, users, especially keyboard users, cannot tell whether the editor holds input focus. An alternative like focus-within:ring-2 should always be added.
9Why do Tailwind's preflight reset and ProseMirror's base classes sometimes conflict?
Both systems try to style the same elements, like headings and lists. Consistently relying on the prose class as the single source of typographic base rules avoids specificity conflicts.
10Should the toolbar wrap or scroll on narrow screens?
Horizontal scrolling is usually the better choice, because wrapped toolbar rows eat into valuable vertical space needed for the actual editing area.