React & Next.js Guide · 9 min read

How to Render Markdown Tables in React & Next.js

Whether you are building an AI chat assistant (ChatGPT/Claude interface), documentation site, or developer blog, rendering Markdown tables in React is notoriously prone to unformatted strings and missing borders. Here is the definitive guide to getting pixel-perfect, responsive tables with react-markdown, remark-gfm, and Tailwind CSS.

1. The Core Problem: Why Tables Silently Fail Out of the Box

Almost every developer who installs react-markdown encounters this frustrating symptom:

// Broken Output: Renders as raw pipe strings on a single line!

| Name | Role | Status | | Alex | Lead | Active | | Sarah | Dev | Active |

Why this happens: By default, react-markdown is built strictly upon the CommonMark 0.30 specification. CommonMark core intentionally does not include tables, strikethrough, or task lists. Tables were popularized as an extension by GitHub Flavored Markdown (GFM).

The Solution: You must install the remark-gfm plugin and pass it into the remarkPlugins prop.

2. Step-by-Step Setup & Minimal Working Example

First, install both react-markdown and remark-gfm:

# Using npm

npm install react-markdown remark-gfm

# Or using pnpm / yarn

pnpm add react-markdown remark-gfm

Now import remarkGfm and supply it in your React component:

// MarkdownRenderer.tsx
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';

interface Props {
  content: string;
}

export const MarkdownRenderer: React.FC<Props> = ({ content }) => {
  return (
    <ReactMarkdown remarkPlugins={[remarkGfm]}>
      {content}
    </ReactMarkdown>
  );
};

This immediately transforms | col | into native HTML <table>, <thead>, <tbody>, and <tr> elements.

3. Custom Tailwind CSS Component Overrides (Dark Mode & Zebra Striping)

Because Tailwind CSS resets all browser table styles to blank by default, you need clean component overrides. You can map custom styled elements directly through the components prop:

// Complete Beautiful Table Component Map
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';

export function StyledMarkdown({ content }: { content: string }) {
  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      components={{
        // 1. Responsive Horizontal Scroll Wrapper
        table: ({ node, ...props }) => (
          <div className="w-full my-6 overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-800 shadow-xs">
            <table className="w-full text-left text-xs border-collapse" {...props} />
          </div>
        ),
        // 2. Header Style with Clean Contrast
        thead: ({ node, ...props }) => (
          <thead className="bg-slate-50 dark:bg-slate-900/80 border-b border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-200 font-semibold uppercase tracking-wider" {...props} />
        ),
        // 3. Header Cells with Padding & Monospace Friendly Alignment
        th: ({ node, ...props }) => (
          <th className="px-4 py-3 font-bold text-[11px]" {...props} />
        ),
        // 4. Zebra-Striped Data Rows
        tr: ({ node, ...props }) => (
          <tr className="border-b border-slate-100 dark:border-slate-800/60 last:border-0 even:bg-slate-50/50 dark:even:bg-slate-900/40 hover:bg-sky-50/40 dark:hover:bg-sky-950/20 transition-colors" {...props} />
        ),
        // 5. Data Cells
        td: ({ node, ...props }) => (
          <td className="px-4 py-3 text-slate-600 dark:text-slate-300 leading-normal" {...props} />
        ),
      }}
    >
      {content}
    </ReactMarkdown>
  );
}

4. Solving Mobile Viewport Disruption

A common pitfall on mobile devices is a wide table forcing the entire web page to horizontally scroll, breaking headers and navigation bars.

❌ Broken Pattern

Directly styling <table className="w-full"> without an outer overflow wrapper.

Causes 320px-390px mobile screens to push content offscreen.

✅ Correct Pattern

Wrapping the table in a <div className="w-full overflow-x-auto"> container.

Isolates horizontal swiping exclusively to the table block.

5. Adding a "Copy Table as Markdown" Feature

If you are building an AI chatbot (using Vercel AI SDK, LangChain, or custom APIs), users love being able to 1-click copy tables generated by the LLM into Excel or Markdown notes:

'use client';
import React, { useState } from 'react';
import { Copy, Check } from 'lucide-react';

export function TableWrapper({ children, rawMarkdown }: { children: React.ReactNode; rawMarkdown?: string }) {
  const [copied, setCopied] = useState(false);

  const handleCopy = () => {
    if (!rawMarkdown) return;
    navigator.clipboard.writeText(rawMarkdown);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <div className="relative group my-4">
      {rawMarkdown && (
        <button
          onClick={handleCopy}
          className="absolute right-2 top-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity px-2 py-1 text-[11px] font-semibold bg-white/90 dark:bg-slate-800/90 border border-slate-200 dark:border-slate-700 rounded-md shadow-xs flex items-center gap-1 text-slate-700 dark:text-slate-300"
        >
          {copied ? <Check className="w-3 h-3 text-emerald-500" /> : <Copy className="w-3 h-3" />}
          {copied ? 'Copied' : 'Copy Markdown'}
        </button>
      )}
      <div className="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-800">
        {children}
      </div>
    </div>
  );
}

6. Top 5 Troubleshooting Gotchas

1. Missing Blank Line Before Table in Streaming LLM Responses

GFM table parsers require a blank line immediately preceding the table header. If your streaming response sends Here are results:\n| Col 1 | without two line breaks (\n\n), the parser will treat the table as a paragraph. Pre-process your stream to ensure double newlines before pipe blocks.

2. React Hydration Mismatch Warnings

If your table contains nested HTML or unescaped characters rendered differently between server and client, React will warn of a hydration mismatch. Use strict server component rendering or wrap the component in a client wrapper with an isMounted guard.

3. Tailwind Typography (@tailwindcss/typography) Border Overrides

If your wrapper uses the prose class, Tailwind sets border-bottom on cells automatically. To enforce your custom design, add prose-table:border-collapse prose-th:px-4 prose-td:px-4 to the parent container.

Need to format or validate Markdown tables before feeding them to React?

Test your tables in our interactive formatter or validate unescaped pipes with 1-click automatic fix.