Build a Streaming AI Chat UI in React with shadcn/ui (2026 Guide) A developer published a guide for building a streaming AI chat interface in React using shadcn/ui, the Vercel AI SDK, and four open-source components from Cuelark UI (Conversation, Message, StreamingText, and PromptInput). The guide frames the work around four UX problems that streaming introduces—scroll ownership, interruptibility, visual stability, and screen-reader announcements—and maps each component to one of them, with components installed as editable source via the shadcn registry. Getting tokens from a model to the browser is the easy part. Every modern SDK does it in a few lines. The hard part is everything the user actually experiences while those tokens arrive: the view that yanks them back to the bottom while they are trying to read, the Send button that does nothing while a reply runs, the paragraph that reflows every 40 milliseconds, the screen reader that either announces nothing or announces every single token. This guide builds a streaming chat interface in React that gets those details right. We will use shadcn/ui as the foundation, the Vercel AI SDK for the transport, and four open-source components from Cuelark UI for the parts shadcn does not ship: Conversation , Message , StreamingText and PromptInput . Every component is installed as source, so if you disagree with a decision we made, you can change the file. By the end you will have: Before writing code, it helps to name the problems. A static list of messages is a solved problem. Streaming adds four new ones. Scroll ownership. While a reply streams, the view should stick to the bottom. The instant the reader scrolls up, the interface must stop chasing new tokens, because the reader has told you they want to read something else. When they want to come back, they need a fast way to do it. Interruptibility. A reply in progress is a running process. If the model has misunderstood the question, the user should be able to stop it immediately, from the keyboard as well as the mouse. A text box with no Stop button during streaming feels broken. Visual stability. Each chunk that arrives can cause the paragraph to reflow. Words that were already read should not move, flash or re-render with a different weight. Announcements. Assistive technology should hear that a reply is arriving without being flooded token by token. The four components below map one to one onto those problems. You need a React project configured for shadcn/ui with Tailwind CSS v4. Next.js, Vite, TanStack Start, React Router and Astro all work. If you are starting fresh: npx shadcn@latest init Then add the Cuelark registry to components.json once, so you can install components by name: { "registries": { "@cuelark": "https://ui.cuelark.com/r/{name}.json" } } Now install the four chat components: npx shadcn@latest add @cuelark/conversation @cuelark/message @cuelark/streaming-text @cuelark/prompt-input If you would rather not touch components.json , each component also installs by URL, for example npx shadcn@latest add https://ui.cuelark.com/r/conversation.json . The CLI brings the theme tokens and the motion layer along with whatever you add. The full set of options is on the installation page https://ui.cuelark.com/docs/installation . Finally, install the AI SDK packages: npm install ai @ai-sdk/react @ai-sdk/openai The examples use OpenAI, but any AI SDK provider works the same way. The server side is short. This is a Next.js App Router route; the shape is similar for other frameworks. js // app/api/chat/route.ts import { openai } from "@ai-sdk/openai"; import { streamText, convertToModelMessages, type UIMessage } from "ai"; export async function POST req: Request { const { messages }: { messages: UIMessage } = await req.json ; const result = streamText { model: openai "gpt-4o-mini" , messages: convertToModelMessages messages , } ; return result.toUIMessageStreamResponse ; } The snippets here are written against AI SDK 5's useChat and UI message stream. If you are on a different major version, the hook names may differ slightly; the UI layer does not change. Here is the skeleton of the chat page. js "use client"; import { useChat } from "@ai-sdk/react"; import { Conversation, ConversationContent } from "@/components/ui/conversation"; import { Message, MessageContent } from "@/components/ui/message"; import { StreamingText } from "@/components/ui/streaming-text"; import { PromptInput } from "@/components/ui/prompt-input"; export default function Chat { const { messages, sendMessage, status, stop } = useChat ; const isStreaming = status === "submitted" || status === "streaming"; return