Markdown

Render streaming markdown with headings, lists, tables, blockquotes, and code fences. External links get safe target/rel handling.

Release Notes

Welcome to the Agent Elements showcase! Here's a breakdown of the latest features and improvements designed to enhance your development workflow.

✨ Key Highlights

  • Streaming Markdown: Real-time rendering of assistant responses.
  • Interactive Tool Cards: Native UI for Bash, Search, and planning tools.
  • Responsive Layouts: Optimized for both side-by-side and mobile views.

📊 Component Status

ComponentStatusPriority
MessageList✅ ReadyHigh
InputBar🧪 BetaMedium
Markdown✅ ReadyHigh

🛠️ Implementation Example

Integrating the Markdown component is straightforward:

tsx
import { Markdown } from '@/components/agent-elements/components/markdown';
export function ChatMessage({ content }) {  return <Markdown content={content} />;}

[!TIP] Use the className prop to customize the typography and spacing of the rendered markdown.


Last updated: March 2026

Getting Started
$pnpm dlx shadcn@latest add https://agent-elements.21st.dev/r/markdown.json
Examples
Release note snippet

Release notes

  • Added streaming markdown
  • Improved tool rendering

Example

Use "Markdown" to render assistant text as it streams.

ts
console.log("Hello from markdown");
Streaming update
Source

Copy and paste the following code into your project, or run the install command above to pull it in automatically.

components/markdown.tsx
"use client";

import { Streamdown, type Components } from "streamdown";
import { createCodePlugin } from "@streamdown/code";
import { cn } from "../utils/cn";

function fixNumberedListBreaks(text: string): string {
  return text.replace(/^(\d+)\.\s*\n+\s*\n*/gm, "$1. ");
}

const CODE_FENCE_LANGS = new Set([
  "bash",
  "diff",
  "html",
  "js",
  "json",
  "jsx",
  "md",
  "markdown",
  "sh",
  "shell",
  "text",
  "ts",
  "tsx",
  "yml",
  "yaml",
]);

function normalizeCodeFenceLanguages(text: string): string {
  return text.replace(/```([^\n]*)/g, (_match, langRaw) => {
    const lang = String(langRaw || "")
      .trim()
      .toLowerCase();
    if (!lang) return "```";
    const normalized = lang.split(/\s+/)[0];
    return CODE_FENCE_LANGS.has(normalized) ? `\`\`\`${normalized}` : "```text";
  });
}

export type MarkdownProps = {
  content: string;
  className?: string;
  textContrast?: "normal" | "high";
};

const code = createCodePlugin({
  themes: ["github-light", "github-dark"],
});

export function Markdown({ content, className }: MarkdownProps) {
  const safeContent = normalizeCodeFenceLanguages(
    fixNumberedListBreaks(content),
  );
  const components: Components = {
    h1: ({ children, ...props }) => (
      <h1 className="an-md-h1 text-base font-semibold mt-3 mb-1.5" {...props}>
        {children}
      </h1>
    ),
    h2: ({ children, ...props }) => (
      <h2 className="an-md-h2 text-base font-semibold mt-3 mb-1.5" {...props}>
        {children}
      </h2>
    ),
    h3: ({ children, ...props }) => (
      <h3 className="an-md-h3 text-sm font-semibold mt-2 mb-1" {...props}>
        {children}
      </h3>
    ),
    h4: ({ children, ...props }) => (
      <h4 className="an-md-h4 text-sm font-medium mt-2 mb-1" {...props}>
        {children}
      </h4>
    ),
    p: ({ children, ...props }) => (
      <p
        className="an-md-p text-sm leading-relaxed text-an-foreground/80"
        {...props}
      >
        {children}
      </p>
    ),
    ul: ({ children, ...props }) => (
      <ul
        className="an-md-ul list-disc list-outside space-y-0.5 text-sm mb-2 pl-4 text-an-foreground/80"
        {...props}
      >
        {children}
      </ul>
    ),
    ol: ({ children, ...props }) => (
      <ol
        className="an-md-ol list-decimal list-outside space-y-0.5 text-sm mb-2 pl-5 text-an-foreground/80"
        {...props}
      >
        {children}
      </ol>
    ),
    li: ({ children, ...props }) => (
      <li className="an-md-li text-sm pl-0.5 text-an-foreground/80" {...props}>
        {children}
      </li>
    ),
    strong: ({ children, ...props }) => (
      <strong className="font-medium text-an-foreground" {...props}>
        {children}
      </strong>
    ),
    a: ({ href, children, ...props }) => {
      if (!href) return <span>{children}</span>;
      const isExternal = href.startsWith("http") || href.startsWith("mailto:");
      return (
        <a
          {...props}
          href={href}
          target={isExternal ? "_blank" : undefined}
          rel={isExternal ? "noopener noreferrer" : undefined}
          className="an-md-link hover:underline underline-offset-2 text-an-primary-color"
        >
          {children}
        </a>
      );
    },
    blockquote: ({ children, ...props }) => (
      <blockquote
        className="an-md-blockquote pl-3 italic mb-2 text-sm border-l-2 border-an-border-color text-an-foreground/70"
        {...props}
      >
        {children}
      </blockquote>
    ),
    hr: ({ ...props }) => (
      <hr className="an-md-hr my-4 border-an-border-color" {...props} />
    ),
    table: ({ children, ...props }) => (
      <div className="overflow-x-auto my-3 border border-an-border-color rounded-an-tool-border-radius">
        <table
          className="an-md-table w-full text-sm [&>thead]:bg-an-tool-background [&>thead>tr>th]:bg-an-tool-background"
          {...props}
        >
          {children}
        </table>
      </div>
    ),
    th: ({ children, ...props }) => (
      <th
        className="text-left font-medium px-3 py-2 bg-an-background-secondary"
        {...props}
      >
        {children}
      </th>
    ),
    td: ({ children, ...props }) => (
      <td
        className="px-3 py-2 border-t border-an-border-color text-an-foreground/80"
        {...props}
      >
        {children}
      </td>
    ),
  };

  return (
    <div
      className={cn(
        "an-markdown",
        "overflow-hidden wrap-break-word",
        "[&_li>p]:inline [&_li>p]:mb-0",
        className,
      )}
    >
      <Streamdown components={components} plugins={{ code }}>
        {safeContent}
      </Streamdown>
    </div>
  );
}
API reference
Prop
Type
Required
content
string
Yes
className
string
No
textContrast
"normal" | "high"
No