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