Next.js

Markdown Chatbot with Streamdown

Render streaming Markdown responses with Streamdown, Next.js, and the AI SDK.

Streamdown renders Markdown as it streams from a language model. It handles incomplete Markdown syntax and memoizes Markdown blocks so unchanged blocks can skip re-rendering. Streamdown provides this behavior in its default streaming mode.

Start with the Stream Text with Chat Prompt recipe for the /api/chat route. Then use Streamdown to render the text parts in the client.

Installation

Install Streamdown in your Next.js application:

npm install streamdown

For Tailwind CSS v4, add Streamdown to the sources in your global stylesheet:

app/globals.css
@import 'tailwindcss';
@source '../node_modules/streamdown/dist/*.js';

The @source path is relative to the stylesheet. Adjust it if you use a src/app directory or a monorepo. See the Streamdown installation guide for Tailwind CSS v3 configuration.

Client

Pass each text part to Streamdown. Set isAnimating when that part is streaming so Streamdown can disable interactive controls until the text is complete.

app/page.tsx
'use client';

import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';
import { Streamdown } from 'streamdown';

export default function Page() {
  const [input, setInput] = useState('');
  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({ api: '/api/chat' }),
  });

  return (
    <div>
      {messages.map(message => (
        <div key={message.id}>
          <div>{message.role === 'user' ? 'You' : 'Assistant'}</div>
          {message.parts.map((part, index) =>
            part.type === 'text' ? (
              <Streamdown
                key={`${message.id}-${index}`}
                isAnimating={part.state === 'streaming'}
              >
                {part.text}
              </Streamdown>
            ) : null,
          )}
        </div>
      ))}

      <form
        onSubmit={event => {
          event.preventDefault();
          sendMessage({ text: input });
          setInput('');
        }}
      >
        <input
          value={input}
          onChange={event => setInput(event.target.value)}
          placeholder="Say something..."
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

For syntax highlighting, math, or Mermaid diagrams, see Streamdown's plugins.