caliper uidocs

Streaming response

Free

Markdown-like text that renders cleanly while it is still arriving.

Installs components/ui/streaming-response.tsxDepends on @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/streaming-response.json

Review of the counter change

The change is safe to merge with one note. It adds a step prop, caps the value at maxCount, and announces the new count to screen readers.

  • The functional update avoids stale state when you click quickly.
  • Disabling the button at the cap is clearer than silently ignoring clicks.
  • maxCount is declared after the component. That works, but moving it above would read better.

Here is a small test you could add:

ts
ts
it("stops at the maximum", () => {  render(<Counter start={98} step={5} />);  userEvent.click(screen.getByRole("button", { name: "Add" }));  expect(screen.getByText("Count: 99")).toBeInTheDocument();});

See the testing notes for the full checklist.

When to use #

Use it for

  • Showing a model's answer token by token without layout jumps.
  • Mixed paragraphs, lists and code blocks.

Skip it when

  • The full text is known up front; render it directly.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/streaming-response.json

Adds its 5 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above. It is built from StreamingResponse and parseBlocks.

example.tsx
import { useState } from "react"
import { StreamingResponse } from "@/components/ui/streaming-response"

const after = `import { useState } from "react";

export function Counter({ start = 0, step = 1 }: { start?: number; step?: number }) {
  const [count, setCount] = useState(start);
  const label = \`Count: \${count}\`;

  function increment() {
    setCount((current) => Math.min(current + step, maxCount));
  }

  return (
    <div className="counter">
      <p aria-live="polite">{label}</p>
      <button onClick={increment} disabled={count >= maxCount}>
        Add
      </button>
    </div>
  );
}

export const defaultStart = 0;
export const maxCount = 99;
`;

const reply = `## Review of the counter change

The change is **safe to merge** with one note. It adds a \`step\` prop, caps the value at \`maxCount\`, and announces the new count to screen readers.

- The functional update avoids stale state when you click quickly.
- Disabling the button at the cap is clearer than silently ignoring clicks.
- \`maxCount\` is declared after the component. That works, but moving it above would read better.

Here is a small test you could add:

\`\`\`ts
it("stops at the maximum", () => {
  render(<Counter start={98} step={5} />);
  userEvent.click(screen.getByRole("button", { name: "Add" }));
  expect(screen.getByText("Count: 99")).toBeInTheDocument();
});
\`\`\`

See [the testing notes](https://example.com/notes) for the full checklist.`;

export function StreamingResponseDemo() {
  const [shown] = useState(reply.length);
  const [running] = useState(false);
  return (
    <StreamingResponse text={reply.slice(0, shown)} streaming={running} pending={running && shown === 0} />
  );
}

API reference #

StreamingResponse

component
PropTypeDefaultDescription
text *string—
streamingbooleanfalse
pendingbooleanfalseNothing has arrived yet.
classNamestring—

parseBlocks

component

No props of its own; it accepts the native props of its root element.

Accessibility #

It is built on native elements, so roles and focus come from the browser.

No custom key handling; Tab and the native activation keys do the work.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.