Skip to content
Learn Agentic AI
Learn Agentic AI11 min read0 views

Chat Message Rendering: Markdown, Code Blocks, Tables, and Rich Content

Build a rich message renderer for AI agent chat interfaces that handles markdown, syntax-highlighted code blocks, tables, and embedded images using React and TypeScript.

The Challenge of Agent Message Content

AI agents produce rich output: code snippets in multiple languages, data tables, mathematical notation, step-by-step instructions with nested lists, and inline references. Rendering this content faithfully in a chat bubble requires a markdown pipeline that handles edge cases gracefully without introducing security vulnerabilities through raw HTML injection.

Setting Up the Markdown Pipeline

The react-markdown library provides a solid foundation. Combine it with remark-gfm for GitHub Flavored Markdown (tables, strikethrough, task lists) and a syntax highlighting library for code blocks.

flowchart TD
    START["Chat Message Rendering: Markdown, Code Blocks, Ta…"] --> A
    A["The Challenge of Agent Message Content"]
    A --> B
    B["Setting Up the Markdown Pipeline"]
    B --> C
    C["Syntax-Highlighted Code Blocks"]
    C --> D
    D["The Copy Button"]
    D --> E
    E["Styled Tables"]
    E --> F
    F["Safe Image Rendering"]
    F --> G
    G["Preventing XSS in Rendered Content"]
    G --> H
    H["FAQ"]
    H --> DONE["Key Takeaways"]
    style START fill:#4f46e5,stroke:#4338ca,color:#fff
    style DONE fill:#059669,stroke:#047857,color:#fff
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";

interface MessageRendererProps {
  content: string;
}

function MessageRenderer({ content }: MessageRendererProps) {
  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      components={{
        code: CodeBlock,
        table: StyledTable,
        img: SafeImage,
      }}
    >
      {content}
    </ReactMarkdown>
  );
}

The components prop lets you override how each markdown element renders. This is where you add syntax highlighting, custom table styles, and image handling.

Syntax-Highlighted Code Blocks

The code component must distinguish between inline code and fenced code blocks. Fenced blocks have a className prop containing the language.

import { ComponentPropsWithoutRef } from "react";

function CodeBlock({
  children,
  className,
  ...props
}: ComponentPropsWithoutRef<"code">) {
  const match = /language-(\w+)/.exec(className || "");

  if (!match) {
    return (
      <code
        className="bg-gray-100 text-red-600 px-1.5 py-0.5
                   rounded text-sm font-mono"
        {...props}
      >
        {children}
      </code>
    );
  }

  const language = match[1];
  const codeString = String(children).replace(/\n$/, "");

  return (
    <div className="relative group my-3">
      <div className="flex items-center justify-between
                      bg-gray-800 text-gray-300 px-4 py-1.5
                      rounded-t-lg text-xs">
        <span>{language}</span>
        <CopyButton text={codeString} />
      </div>
      <SyntaxHighlighter
        style={oneDark}
        language={language}
        PreTag="div"
        customStyle={{
          margin: 0,
          borderTopLeftRadius: 0,
          borderTopRightRadius: 0,
        }}
      >
        {codeString}
      </SyntaxHighlighter>
    </div>
  );
}

The Copy Button

Every code block needs a copy button. Implement it with the Clipboard API and visual feedback.

import { useState, useCallback } from "react";

function CopyButton({ text }: { text: string }) {
  const [copied, setCopied] = useState(false);

  const handleCopy = useCallback(async () => {
    await navigator.clipboard.writeText(text);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  }, [text]);

  return (
    <button
      onClick={handleCopy}
      className="text-xs text-gray-400 hover:text-white
                 transition-colors"
    >
      {copied ? "Copied!" : "Copy"}
    </button>
  );
}

Styled Tables

Agent responses frequently include comparison tables, data summaries, and feature matrices. Default HTML tables look terrible without styling.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

import { ComponentPropsWithoutRef } from "react";

function StyledTable({
  children,
  ...props
}: ComponentPropsWithoutRef<"table">) {
  return (
    <div className="overflow-x-auto my-3 rounded-lg border">
      <table
        className="min-w-full divide-y divide-gray-200
                   text-sm"
        {...props}
      >
        {children}
      </table>
    </div>
  );
}

Add matching overrides for th and td elements with padding, borders, and alternating row colors to complete the table styling.

Safe Image Rendering

Agent responses may reference images. Render them with size constraints and error handling so that broken image links do not break the entire chat layout.

import { useState } from "react";

function SafeImage(props: React.ImgHTMLAttributes<HTMLImageElement>) {
  const [error, setError] = useState(false);

  if (error) {
    return (
      <div className="border rounded-lg p-3 text-sm text-gray-500 my-2">
        Image could not be loaded
      </div>
    );
  }

  return (
    <img
      {...props}
      onError={() => setError(true)}
      className="max-w-full h-auto rounded-lg my-2"
      loading="lazy"
    />
  );
}

Preventing XSS in Rendered Content

react-markdown does not render raw HTML by default, which is the safest behavior. If you enable the rehype-raw plugin to support HTML in agent responses, you must pair it with rehype-sanitize to strip dangerous elements like <script> tags and event handlers. For most agent interfaces, keeping raw HTML disabled is the better choice.

FAQ

How do I handle LaTeX or mathematical notation in agent responses?

Install remark-math and rehype-katex, then add them to the remarkPlugins and rehypePlugins arrays respectively. This renders inline math with single dollar signs ($x^2$) and block math with double dollar signs. Import the KaTeX CSS stylesheet to style the rendered equations.

How do I prevent very long code blocks from making the chat bubble too wide?

The overflow-x-auto class on the code container enables horizontal scrolling when code lines are wider than the bubble. Set word-break: break-all on inline code to prevent long strings without spaces from overflowing. For block code, never set white-space: pre-wrap because it breaks code indentation.

Should I memoize the message renderer?

Yes. Wrap MessageRenderer in React.memo because chat messages are immutable after they are fully streamed. Without memoization, every new message appended to the list causes all previous messages to re-render their full markdown pipeline, which becomes expensive with syntax highlighting across dozens of messages.


#Markdown #SyntaxHighlighting #React #TypeScript #RichContent #AgenticAI #LearnAI #AIEngineering

Share
C

Written by

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.

Related Articles You May Like

Learn Agentic AI

Building Your First MCP Server: Connect AI Agents to Any External Tool

Step-by-step tutorial on building an MCP server in TypeScript, registering tools and resources, handling requests, and connecting to Claude and other LLM clients.

Learn Agentic AI

How to Build an AI Coding Assistant with Claude and MCP: Step-by-Step Guide

Build a powerful AI coding assistant that reads files, runs tests, and fixes bugs using the Claude API and Model Context Protocol servers in TypeScript.

Learn Agentic AI

Building an Agent Playground: Interactive Testing Environment for Prompt and Tool Development

Build a full-featured agent playground with a web UI that lets you test prompts live, tune parameters, compare model outputs side by side, and export working configurations for production deployment.

Learn Agentic AI

Generative UI with AI Agents: Dynamically Creating React Components from Natural Language

Explore how the Vercel AI SDK's generativeUI capability lets AI agents stream fully interactive React components to users, replacing static text responses with dynamic, data-rich interfaces.

Learn Agentic AI

Building an MCP Server in TypeScript: Node.js Tools for AI Agents

Create a fully typed MCP server in TypeScript using the official MCP SDK, with tool handlers, Zod validation, and deployment strategies for exposing Node.js services to AI agents.

Learn Agentic AI

Building a Real-Time AI Coding Assistant: Streaming Code Suggestions and Explanations

Build a real-time AI coding assistant that integrates with code editors, extracts context intelligently, debounces user input, and streams code suggestions and explanations with low latency.