Skip to main content
Streamdown can display a cursor character at the end of the last rendered element while content is actively streaming. Carets make it visually clear to users that generation is still in progress — similar to a blinking cursor in a terminal.

Usage

Pass the caret prop with either "block" or "circle":
chat.tsx
import { Streamdown } from 'streamdown';

function StreamingMessage({ content, isStreaming }: Props) {
  return (
    <Streamdown
      caret="block"
      isAnimating={isStreaming}
    >
      {content}
    </Streamdown>
  );
}
The caret only appears when both conditions are true:
  1. The caret prop is set to "block" or "circle"
  2. isAnimating is true
When streaming stops (isAnimating becomes false), the caret disappears automatically.

Caret styles

caret
"block" | "circle"
Selects the cursor character to display.
  • "block" — renders the vertical bar character , similar to a terminal block cursor
  • "circle" — renders the filled circle character , a subtler dot indicator
<Streamdown caret="block" isAnimating={true}>
  Streaming content...
</Streamdown>
// Renders: Streaming content... ▋

Conditional display in chat UIs

Streamdown has no concept of message order or role. In a chat interface, you typically want the caret to appear only on the last assistant message while it is being streamed:
chat.tsx
{messages.map((message, index) => (
  <Streamdown
    key={message.id}
    caret={
      message.role === 'assistant' &&
      index === messages.length - 1
        ? 'block'
        : undefined
    }
    isAnimating={isStreaming}
  >
    {message.content}
  </Streamdown>
))}

When the caret is hidden

Even when both caret and isAnimating={true} are set, Streamdown suppresses the caret in two situations:
  • Inside an incomplete code fence — when the last block contains an open ``` without a closing ```, the caret is hidden to avoid rendering inside a highlighted code block
  • Inside a table — when the last block contains a table, the caret is hidden to avoid disrupting table layout
Once the code fence or table closes, the caret reappears.

How it works

The caret is implemented with a CSS custom property and a pseudo-element:
  1. When caret and isAnimating are both set, Streamdown writes the caret character to the CSS variable --streamdown-caret on the container element:
    /* Written by Streamdown internally */
    --streamdown-caret: " ▋";  /* for caret="block" */
    --streamdown-caret: " ●";  /* for caret="circle" */
    
  2. A ::after pseudo-element on the last child reads the variable and renders inline:
    /* Applied by Streamdown to the container */
    [&>*:last-child]::after {
      content: var(--streamdown-caret);
      display: inline;
      vertical-align: baseline;
    }
    
  3. When streaming ends or caret is undefined, the CSS variable and the class that enables the pseudo-element are removed, so no extra DOM nodes are left behind.
This approach is efficient: no extra DOM elements, no JavaScript timers, and no re-renders just to toggle the cursor.
The caret does not blink by default. If you want a blinking animation, you can target the ::after pseudo-element with a CSS animation using a [data-streamdown] selector or a scoped className.

Build docs developers (and LLMs) love