Skip to content

Chat

Build AI chat interfaces with components that handle streaming answers.

import { ChatCitation, ChatSource, ChatSources } from '@youj/react'

Installation & usage

npm i @youj/react

For 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.

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 log that 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 article named 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.