Chat
Build AI chat interfaces with components that handle streaming answers.
import { ChatCitation, ChatSource, ChatSources } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
The whole kit is one registry item and one import. It's built on Youj's own Scroll Area, Button, Input Group, Hover Card and Collapsible, and on motion's Presence List, Collapse, Morph Label and Stagger.
The chat kit is the set of parts an assistant is made of: a conversation that stays with a streaming answer, messages with actions, streamed markdown, a composer, thinking and reasoning, tool calls, citations, suggestions and a first-run greeting. It doesn't talk to a model. You bring the stream from any SDK and hand it to the parts, and they handle the hard details: following the answer without fighting the reader, announcing a reply once instead of token by token, and keeping a model's output from ever becoming markup.
A screen is a ChatMessageList of keyed ChatMessages over a PromptInput. Whatever the assistant does before its text (thinking, reasoning, tool calls) goes inside its message as children.
import { ChatMessage, ChatMessageList, ChatToolCall, PromptInput } from '@youj/react'
export function Assistant({ messages, status, send, stop }: AssistantProps) {
return (
<div className="flex h-dvh flex-col">
<ChatMessageList>
{messages.map((m) => (
<ChatMessage key={m.id} role={m.role} content={m.text} streaming={m.streaming}>
{m.tools.map((t) => (
<ChatToolCall key={t.id} name={t.name} args={t.args} status={t.status} result={t.result} />
))}
</ChatMessage>
))}
</ChatMessageList>
<PromptInput status={status} onStop={stop} onSubmit={({ text }) => send(text)} />
</div>
)
}content takes a string, or a ChatTextSource: any object with getSnapshot() and subscribe(). Pass a string and every token re-renders the message. Pass a source and a token re-renders only the text that reads it, not the message, its siblings or the list. createTextStream() is the smallest source you can write to.
import { createTextStream } from '@youj/react'
const stream = createTextStream()
for await (const token of reply) stream.append(token)Keep one stream per reply, created when the reply starts, and set streaming to false when it ends. That's when the list announces the whole answer.
Description
A full conversation: a short history, a streamed answer with reasoning and a tool call, and a composer.
In the preview, a question goes out on load. The answer thinks out loud, calls a tool, then streams markdown with code and a table while the list follows it. Scroll up mid-answer to take over, or press stop.
Code
chat-demo.tsx, the Demo story. The component itself comes from the package.
Reference
The partsMessage List, Message, Markdown… (10 parts)
Reduced motionThe list jumps instead of gliding, messages appear in place, and…
Reduced motion
The list jumps instead of gliding, messages appear in place, and streamed text arrives in whole sentences instead of word by word. Loops (the shimmer, the dots, the caret's blink) hold still, and every fold and morph is instant.
AccessibilityKeyboard, announcements
Accessibility
- The conversation is a
logthat stays silent while an answer streams. The list speaks for it: "Youj is responding…" once, the whole answer when it's complete, and failures assertively. - Every message is an
articlenamed by its author and time, so screen reader users can move between messages by landmark or heading. - Actions on a message sit in one toolbar: a single tab stop, with arrow keys inside it.
- Enter sends and Shift+Enter breaks the line, and neither cuts an IME composition short.