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