Skip to content
nexubDesign System
v2.4.0

Getting Started

Building AI surfaces

A small kit for chat and assistant UIs. The components only render state: you own the request, the stream and the message list. They work with any model or SDK.

The kit

ComponentWhat it does
ConversationScrolling message list with a sticky composer. Follows new content while the reader is at the bottom.
PromptInputThe composer. Send turns into Stop while a reply streams.
MessageBubbleOne message; MessageActions adds copy, feedback and regenerate.
ThinkingIndicatorFills the gap between sending and the first token.
StreamingTextRenders text as it arrives without reflowing what is already there.
Citation, SourceListNumbered source markers and the source row under a reply.

A reply moves through three states, and each has one component. Drive them from a single status value and pass it to PromptInput as well:

<Conversation composer={<PromptInput status={status} onSubmit={send} onStop={stop} />}>
  {messages.map((m) => (
    <MessageBubble key={m.id} from={m.from}>
      {status === 'submitting' && m.id === lastId
        ? <ThinkingIndicator />
        : <StreamingText text={m.text} streaming={status === 'streaming' && m.id === lastId} />}
    </MessageBubble>
  ))}
</Conversation>

The Conversation page has a working version with Stop and a citation.

Motion

  • Motion only shows state. The thinking shape morphs because the assistant is working; new text fades in because it just arrived; the caret blinks because more is coming. Nothing moves for decoration.
  • Text already on screen never moves. StreamingText animates only the newest chunks, and Conversation follows the bottom only while the reader is there.
  • Reduced motion turns it all off: the thinking shape and shimmer are static, chunks appear without fading, the caret stops blinking, and Jump to latest scrolls instantly. The state is still visible.

Accessibility

  • Don’t make streaming text a live region. Screen readers re-read the whole region on every token. StreamingText and Conversation both default to off; keep it that way while tokens arrive.
  • Announce completion once. When a reply finishes, put a short message such as “Reply ready” in a polite status region, or turn on Conversation’s announce if you add whole messages at once instead of streaming.
  • Keep focus in the composer. PromptInput leaves focus in the field after sending; don’t move it to the new message. Users read back with their screen reader’s article navigation, since each MessageBubble is an article.
  • Stop with Escape. While status="streaming", Escape in the field calls onStop, the same as the Stop button.
  • ThinkingIndicator is a polite status: it is announced once when it appears. Remove it when tokens start.

Security

  • Citations only link http: and https: URLs. Anything else, including javascript: and data:, renders as plain text. Use the exported safeUrl() for any other link built from model output.
  • Never render model output as HTML. StreamingText renders text only. If you want Markdown, parse it to React elements with raw HTML disabled; don’t pass it to dangerouslySetInnerHTML.
  • Source links open in a new tab with rel="noopener noreferrer", and favicons load with referrerPolicy="no-referrer".

Message keys

The AI components read these strings from NexubProvider. Each component also takes a messages prop for local overrides.

KeyDefaultUsed by
promptLabel'Message'PromptInput
sendMessage'Send message'PromptInput
stopGenerating'Stop generating'PromptInput
promptHint'Press Enter to send, Shift+Enter for a new line'PromptInput
thinking'Thinking'ThinkingIndicator
youSaid'You said'MessageBubble
assistantSaid'Assistant said'MessageBubble
failedToSend'Failed to send'MessageBubble
retry'Retry'MessageBubble
copy'Copy'MessageActions
copied'Copied'MessageActions
goodResponse'Good response'MessageActions
badResponse'Bad response'MessageActions
regenerate'Regenerate'MessageActions
sources'Sources'SourceList
source(n) => `Source ${n}`Citation
jumpToLatest'Jump to latest'Conversation
conversation'Conversation'Conversation