























Smooth per-word text animation for streaming content.
Streamdown supports per-word streaming animation through the built-in animated prop. Words fade in as they mount, creating a smooth text-reveal effect during AI streaming. When streaming ends, the animation is removed entirely, leaving zero DOM overhead on completed messages.
Import the animation CSS and set the animated prop:
import { Streamdown } from "streamdown";
import "streamdown/styles.css";
export default function Page() {
return (
<Streamdown animated isAnimating={status === "streaming"}>
{markdown}
</Streamdown>
);
}The isAnimating prop controls when the animation is active. When false, the animate plugin is excluded from the rehype pipeline entirely, so completed messages render as plain text with no extra <span> wrappers.
The animation is a rehype transformer that:
<span> elements with data-sd-animatecode, pre, svg, math, and annotation elementsReact's reconciliation ensures only newly-mounted spans trigger the CSS animation. Combined with a short default duration (150ms), this makes batch token arrivals look smooth rather than "chunky."
Three built-in animations are included in styles.css:
A simple opacity transition from invisible to visible.
<Streamdown animated={{ animation: "fadeIn" }} isAnimating={status === "streaming"}>
{markdown}
</Streamdown>Combines opacity with a blur-to-sharp transition. Works well with fast-streaming models where many tokens arrive at once — the blur masks the batch appearance better than pure opacity.
<Streamdown animated={{ animation: "blurIn" }} isAnimating={status === "streaming"}>
{markdown}
</Streamdown>Words fade in while sliding up 4px, creating a subtle rising effect.
<Streamdown animated={{ animation: "slideUp" }} isAnimating={status === "streaming"}>
{markdown}
</Streamdown>Pass an options object to animated to customize animation behavior:
import { Streamdown } from "streamdown";
import "streamdown/styles.css";
export default function Page() {
return (
<Streamdown
animated={{
animation: "blurIn", // "fadeIn" | "blurIn" | "slideUp" | custom string
duration: 200, // milliseconds (default: 150)
easing: "ease-out", // CSS timing function (default: "ease")
sep: "word", // "word" | "char" (default: "word")
}}
isAnimating={status === "streaming"}
>
{markdown}
</Streamdown>
);
}| Option | Type | Default | Description |
|---|---|---|---|
animation | string | "fadeIn" | Animation name. Built-in: fadeIn, blurIn, slideUp. Custom strings are prefixed with sd-. |
duration | number | 150 | Animation duration in milliseconds. |
easing | string | "ease" | CSS timing function. |
sep | "word" | "char" | "word" | Split text by word or character. |
Set sep: "char" to animate each character individually instead of whole words:
<Streamdown animated={{ animation: "fadeIn", sep: "char" }} isAnimating={status === "streaming"}>
{markdown}
</Streamdown>This creates a typewriter-like effect but generates more DOM nodes. Use it sparingly.
Define your own @keyframes and reference them by name:
@keyframes sd-myCustomAnimation {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}<Streamdown animated={{ animation: "myCustomAnimation" }} isAnimating={status === "streaming"}>
{markdown}
</Streamdown>The animation name is automatically prefixed with sd-, so define your keyframes as sd-yourName.
Use onAnimationStart and onAnimationEnd to react to animation state changes. These fire when isAnimating transitions from false to true and vice versa. Both callbacks are suppressed in mode="static".
import { Streamdown } from "streamdown";
import "streamdown/styles.css";
import { useCallback } from "react";
export default function Page() {
const handleAnimationStart = useCallback(() => {
console.log("Streaming started");
}, []);
const handleAnimationEnd = useCallback(() => {
console.log("Streaming ended");
}, []);
return (
<Streamdown
animated
isAnimating={status === "streaming"}
onAnimationStart={handleAnimationStart}
onAnimationEnd={handleAnimationEnd}
>
{markdown}
</Streamdown>
);
}Memoize callbacks with useCallback to avoid unnecessary effect re-runs.
For direct access to the rehype plugin (e.g. in custom pipelines), use createAnimatePlugin:
import { createAnimatePlugin } from "streamdown";
const animate = createAnimatePlugin({
animation: "blurIn",
duration: 200,
});
// animate.rehypePlugin is a standard rehype pluginThe animation skips text inside these elements to avoid breaking their layout:
<code> — inline and block code<pre> — preformatted text<svg> — vector graphics<math> — MathML elements<annotation> — MathML annotationsThis means code blocks, syntax-highlighted code, math equations, and diagrams render without animation spans.
Fast models can dump many tokens per React commit. The default 150ms duration with animation-fill-mode: both ensures words start invisible and end visible, making simultaneous mounts look intentional.
For smoother results with fast models:
blurIn — blur masks batch arrivals better than opacity aloneease-out easing for a more natural deceleration<Streamdown
animated={{
animation: "blurIn",
duration: 250,
easing: "ease-out",
}}
isAnimating={status === "streaming"}
>
{markdown}
</Streamdown>Each animated span receives these CSS custom properties via inline styles:
| Property | Description |
|---|---|
--sd-animation | The @keyframes name to use |
--sd-duration | Animation duration |
--sd-easing | CSS timing function |
The [data-sd-animate] selector in styles.css reads these properties:
[data-sd-animate] {
animation: var(--sd-animation, sd-fadeIn)
var(--sd-duration, 150ms)
var(--sd-easing, ease) both;
}You can override these in your own CSS for more control.
isAnimating此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。