Chat Input
Auto-growing textarea with send on Enter, optional attach/voice, character cap, and stop-generation mode.
Default
Standard composer with max length hint.
0/500
default-example.tsx
1"use client";23import { useState } from "react";4import { ChatInput } from "@/registry/ui/chat-input";56export function ChatInputDefaultExample() {7 const [value, setValue] = useState("");89 return (10 <div className="flex justify-center w-full max-w-lg p-4">11 <ChatInput12 value={value}13 onChange={setValue}14 onSend={() => setValue("")}15 placeholder="Message the team…"16 maxLength={500}17 />18 </div>19 );20}
Generating
Assistant text streams into a bubble; the composer shows stop generation and stays separate from the reply.
Stream runs into the message bubble; the input is only for what you send next.
Assistant reply appears here while it streams.
generating-example.tsx
1"use client";23import { useCallback, useEffect, useRef, useState } from "react";4import { Button } from "@/components/ui/button";5import { ChatBubble } from "@/registry/ui/chat-bubble";6import { ChatInput } from "@/registry/ui/chat-input";7import { ChatTypingIndicator } from "@/registry/ui/chat-typing-indicator";89/** Plain text only - streamed chunks must stay valid while concatenating (avoid half-open markdown). */10const STREAM_DEMO_TEXT =11 "This reply streams into the assistant bubble above, the same place a real model response would land. Your composer below is only for what you send next. Tap the square on the composer to stop generation mid-stream; partial text stays in the bubble.";1213export function ChatInputGeneratingExample() {14 const [draft, setDraft] = useState("");15 const [assistantReply, setAssistantReply] = useState("");16 const [hasStartedOnce, setHasStartedOnce] = useState(false);17 const [isGenerating, setIsGenerating] = useState(false);18 const tickRef = useRef<ReturnType<typeof setInterval> | null>(null);1920 const clearTick = useCallback(() => {21 if (tickRef.current !== null) {22 clearInterval(tickRef.current);23 tickRef.current = null;24 }25 }, []);2627 const finishStream = useCallback(() => {28 clearTick();29 setIsGenerating(false);30 }, [clearTick]);3132 const startStream = useCallback(() => {33 clearTick();34 setAssistantReply("");35 setHasStartedOnce(true);36 setIsGenerating(true);37 let i = 0;38 tickRef.current = setInterval(() => {39 i += 1;40 if (i <= STREAM_DEMO_TEXT.length) {41 setAssistantReply(STREAM_DEMO_TEXT.slice(0, i));42 } else {43 finishStream();44 }45 }, 28);46 }, [clearTick, finishStream]);4748 const stopStream = useCallback(() => {49 clearTick();50 setIsGenerating(false);51 }, [clearTick]);5253 useEffect(() => () => clearTick(), [clearTick]);5455 const streamingPulse =56 isGenerating && assistantReply.length < STREAM_DEMO_TEXT.length;5758 return (59 <div className="mx-auto flex w-full max-w-lg flex-col gap-4 p-4">60 <p className="text-center text-sm text-muted-foreground">61 {isGenerating62 ? "The assistant answer streams in the bubble - stop from the composer’s square button."63 : "Stream runs into the message bubble; the input is only for what you send next."}64 </p>65 <Button66 type="button"67 variant="outline"68 size="sm"69 className="self-center"70 disabled={isGenerating}71 onClick={startStream}72 >73 Simulate streaming reply74 </Button>7576 <div className="rounded-lg border bg-muted/30 px-2 py-3">77 {!hasStartedOnce && (78 <p className="px-2 py-6 text-center text-sm text-muted-foreground">79 Assistant reply appears here while it streams.80 </p>81 )}82 {hasStartedOnce && assistantReply.length === 0 && isGenerating && (83 <div className="px-4 py-2">84 <ChatTypingIndicator />85 </div>86 )}87 {hasStartedOnce && assistantReply.length === 0 && !isGenerating && (88 <p className="px-2 py-6 text-center text-sm text-muted-foreground">89 Stopped before any tokens arrived.90 </p>91 )}92 {hasStartedOnce && assistantReply.length > 0 && (93 <ChatBubble94 content={assistantReply}95 variant="assistant"96 user={{ id: "assistant", name: "Assistant" }}97 showAvatar98 showTimestamp={false}99 showStatus={false}100 isStreaming={streamingPulse}101 className="px-2 py-1"102 />103 )}104 </div>105106 <ChatInput107 value={draft}108 onChange={setDraft}109 disabled={isGenerating}110 isGenerating={isGenerating}111 onStopGeneration={stopStream}112 placeholder={113 isGenerating114 ? "Stop generation with the square - draft locked while streaming…"115 : "Type your next message…"116 }117 />118 </div>119 );120}
Installation & source
Install via the shadcn CLI or copy the registry files manually.
bash
npx shadcn@latest add @tt-ui/chat-input
Props
| Name | Type | Default | Description |
|---|---|---|---|
| value | string | "" | Controlled message text. |
| isGenerating | boolean | false | Swap send for stop and route the primary action to onStopGeneration. |