By Sagar Shankaran, Founder of CallSphere
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.
Key takeaways
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.
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 LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT 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.
The code component must distinguish between inline code and fenced code blocks. Fenced blocks have a className prop containing the language.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
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>
);
}
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>
);
}
Agent responses frequently include comparison tables, data summaries, and feature matrices. Default HTML tables look terrible without styling.
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.
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.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
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"
/>
);
}
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.
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.
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.
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

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
AI SDK 5 ships fully typed chat for React, Svelte, Vue, and Angular plus first-class agent loop primitives. Here are the patterns that matter for shipping in 2026.
Mastra.ai is becoming the go-to TypeScript agent framework in 2026. Workflows, RAG, evals, and an honest comparison with Vercel AI SDK 5 for serious teams.
How to wire Vercel AI SDK 5 tool calls to a React UI with streaming, partial UI updates, and proper error handling that survives flaky network conditions.
Convex's reactive queries auto-push every transcript update to every subscribed client. Real working code for actions, mutations, and OpenAI Realtime streaming.
The MCP TypeScript SDK 1.0 makes server development straightforward and type-safe. A walkthrough from scaffold to a deployed Cloud Run service ready for production traffic.
AI SDK 5 ships agent loops, MCP client, and Generative UI v2 — the cleanest TypeScript stack for shipping AI features in 2026 without rewriting your frontend.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI