Search

Search the site

All components

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
1"use client";
2
3import { useState } from "react";
4import { ChatInput } from "@/registry/ui/chat-input";
5
6export function ChatInputDefaultExample() {
7 const [value, setValue] = useState("");
8
9 return (
10 <div className="flex justify-center w-full max-w-lg p-4">
11 <ChatInput
12 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.

1"use client";
2
3import { 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";
8
9/** 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.";
12
13export 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);
19
20 const clearTick = useCallback(() => {
21 if (tickRef.current !== null) {
22 clearInterval(tickRef.current);
23 tickRef.current = null;
24 }
25 }, []);
26
27 const finishStream = useCallback(() => {
28 clearTick();
29 setIsGenerating(false);
30 }, [clearTick]);
31
32 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]);
47
48 const stopStream = useCallback(() => {
49 clearTick();
50 setIsGenerating(false);
51 }, [clearTick]);
52
53 useEffect(() => () => clearTick(), [clearTick]);
54
55 const streamingPulse =
56 isGenerating && assistantReply.length < STREAM_DEMO_TEXT.length;
57
58 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 {isGenerating
62 ? "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 <Button
66 type="button"
67 variant="outline"
68 size="sm"
69 className="self-center"
70 disabled={isGenerating}
71 onClick={startStream}
72 >
73 Simulate streaming reply
74 </Button>
75
76 <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 <ChatBubble
94 content={assistantReply}
95 variant="assistant"
96 user={{ id: "assistant", name: "Assistant" }}
97 showAvatar
98 showTimestamp={false}
99 showStatus={false}
100 isStreaming={streamingPulse}
101 className="px-2 py-1"
102 />
103 )}
104 </div>
105
106 <ChatInput
107 value={draft}
108 onChange={setDraft}
109 disabled={isGenerating}
110 isGenerating={isGenerating}
111 onStopGeneration={stopStream}
112 placeholder={
113 isGenerating
114 ? "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

NameTypeDefaultDescription
valuestring""Controlled message text.
isGeneratingbooleanfalseSwap send for stop and route the primary action to onStopGeneration.