Skip to main content
All articles
Tutorial July 5, 2026 8 min read

How to Build a Chat UI in React with Ninna UI

A practical guide to building a chat interface in React: message list, input area, streaming responses, and typing indicators — all with Ninna UI components.

By Ninna UI Team


Chat interfaces are everywhere in 2026 — from AI assistants to customer support. Building one that's accessible, responsive, and handles streaming responses requires careful component composition. Let's build one with Ninna UI.

1. Message list

import { Avatar, Text } from "@ninna-ui/primitives";
import { VStack, HStack } from "@ninna-ui/layout";

function Message({ role, content }: { role: "user" | "assistant"; content: string }) {
  const isUser = role === "user";
  return (
    <HStack gap="3" className={isUser ? "flex-row-reverse" : ""}>
      <Avatar name={isUser ? "You" : "AI"} size="sm" />
      <div className={`rounded-2xl p-3 max-w-[80%] ${isUser ? "bg-primary text-primary-content" : "bg-base-200"}`}>
        <Text size="sm">{content}</Text>
      </div>
    </HStack>
  );
}

2. Input area

import { Input, IconButton } from "@ninna-ui/primitives";
import { Send } from "lucide-react";

function ChatInput({ onSend }: { onSend: (text: string) => void }) {
  return (
    <form onSubmit={(e) => { e.preventDefault(); onSend(input); }}>
      <div className="flex gap-2">
        <Input placeholder="Type a message..." />
        <IconButton color="primary" type="submit" aria-label="Send message">
          <Send className="size-4" />
        </IconButton>
      </div>
    </form>
  );
}

3. Typing indicator

import { Loading } from "@ninna-ui/feedback";
import { HStack, Text } from "@ninna-ui/primitives";

function TypingIndicator() {
  return (
    <HStack gap="2">
      <Loading size="sm" />
      <Text size="sm" className="text-base-content/60">AI is typing...</Text>
    </HStack>
  );
}

4. Putting it together

export function ChatUI() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [typing, setTyping] = useState(false);

  return (
    <VStack gap="4" className="max-w-2xl mx-auto p-4">
      <VStack gap="3" className="flex-1 overflow-y-auto">
        {messages.map((msg, i) => <Message key={i} {...msg} />)}
        {typing && <TypingIndicator />}
      </VStack>
      <ChatInput onSend={(text) => {
        setMessages((m) => [...m, { role: "user", content: text }]);
        setTyping(true);
        // Call your API here
      }} />
    </VStack>
  );
}

Ninna UI ships a ready-made AI Chat Interface block at /blocks/ai-chat-interface. It includes message bubbles, streaming support, and a prompt input area — copy it and adapt to your API.

Accessibility considerations

  • Use aria-live="polite" on the message container so screen readers announce new messages
  • The send button should have an aria-label (IconButton does this via the aria-label prop)
  • Auto-scroll to the latest message, but respect prefers-reduced-motion
  • The input should receive focus on page load

Build it with Ninna UI

Accessible React components, CSS-only theming, Tailwind v4 native.

Get Started

Keep reading