React AI Chat UI: Streaming, Auto-Scroll und Mobile

Eine React AI Chat UI sieht nach einem Nachmittag fertig aus: eine Liste mit Bubbles, eine Textarea und ein fetch-Aufruf, der Text streamt. Im echten Einsatz zeigen sich die Lücken. Der Thread zieht den Leser wieder nach unten, während er eine frühere Antwort noch einmal liest. Ein halb geschriebener Code-Fence macht den Rest der Antwort zu Code. Wer mit einer japanischen IME tippt, schickt mit Enter einen halben Satz ab, und auf dem iPhone rutscht der Composer hinter die Tastatur. Dieser Guide zeigt die fünf Teile eines produktionsreifen Chats, was jeder davon abfangen muss und mit welchem Code du sie an dein Modell anbindest. Die Beispiele nutzen die KI-Komponenten von Wingo UI, der Bibliothek, die wir bauen. Wir sind also nicht neutral. Fakten aus anderen Quellen verlinken wir.
Aus welchen Teilen besteht eine React AI Chat UI?
Aus fünf Teilen mit einem gemeinsamen Nachrichtentyp. Jede Komponente einer React AI Chat UI hat genau eine Aufgabe, und bei jeder taucht ein typischer Fehler als Erstes auf:
Der gemeinsame Typ ist ChatMessageData aus lib/ai-chat: eine id, eine role, content als Markdown und ein status mit pending, streaming, complete, stopped oder error, dazu optional reasoning, toolCalls, attachments und sources. Der Status steuert den Großteil des Screens: die Tipp-Punkte, den Textcursor, die Aktionen und das, was ein Screenreader vorliest.
Die CLI holt Chat Message, Reasoning Indicator, Model Picker und Markdown als Abhängigkeiten mit. Coding-Agenten installieren dieselben Items über unseren MCP-Server; der Guide zur Component Library mit MCP zeigt das Setup.
Wie sollte ein Chat-Thread scrollen, während eine Antwort streamt?
Folge neuem Text nur, solange der Leser unten bleibt. Lass sofort los, wenn er nach oben scrollt, und komm zurück, wenn er sendet oder auf einen Sprung-Button tippt. Die Doku zum Message Scroller von shadcn (opens in a new tab) formuliert die Regel so: Wer wegscrollt, wird dort in Ruhe gelassen.
Damit das im Alltag hält:
- Erst die Absicht lesen, dann die Position. Ganz unten hat eine Mausrad-Drehung nach oben
scrollTopnoch nicht verändert, wenn der nächste Chunk ankommt. Der Hook useStickToBottom des Threads lässt schon beim Wheel-Event selbst los, damit eine streamende Antwort den Leser nie zurückzieht. - Außerhalb von React folgen. Das Folgen schreibt
scrollTopin einerrequestAnimationFrame-Schleife, die das Ziel in jedem Frame neu liest. React-State ändert sich nur, wenn „ganz unten“ oder „folgt“ umschlägt. - Senden führt zurück nach unten, auch wenn der Leser vorher hochgescrollt hat.
- Beim Nachladen der Historie bleibt das Auge, wo es ist. Bevor ältere Nachrichten oben eingefügt werden, merkt sich der Thread die erste sichtbare Nachricht und ihren Abstand und stellt beides danach wieder her.
- Die Sprung-Pille zählt mit. Solange du hochgescrollt bist, zeigt eine Pille über dem Composer, wie viele Antworten angekommen sind. Ein Tipp darauf scrollt sanft nach unten und setzt den Fokus auf die neueste Nachricht.
$ npx wingo-ui@latest add chat-threadDer komplette Ablauf mit deiner eigenen Route:
/api/chat ist dein Route Handler. Er muss die Antwort nur als reinen Text-Stream zurückgeben; mit dem AI SDK liefert streamText(...).toTextStreamResponse() (opens in a new tab) genau das. Die wartende Nachricht zeigt Tipp-Punkte bis zum ersten Chunk, und Stop behält den Text, der schon angekommen ist.
Wie streamst du Tokens, ohne jede Nachricht neu zu rendern?
Gib nur der geänderten Nachricht ein neues Objekt, memoisiere die Nachrichten-Komponente und rendere Markdown Block für Block, damit jeder Chunk nur den letzten Block neu parst.
- Memoisierte Nachrichten. Chat Message steckt in
memo, und Chat Thread bindet die Handler jeder Nachricht über eine Ref. Eine fertige Nachricht rendert also nur dann neu, wenn sich ihr eigenes Objekt ändert. LegmessagePropsals Konstante auf Modulebene an oder pack es inuseMemo, damit Labels und Klassennamen zwischen den Chunks ihre Identität behalten. - Markdown reparieren. Der Markdown-Renderer teilt die Antwort in Blöcke auf oberster Ebene. Während des Streamings repariert er das Ende vor dem Parsen: Er schließt einen offenen Code-Fence und nicht geschlossene
**,~~oder Backticks, verwirft ein halb getipptes Link-Ziel und hält eine Tabelle zurück, bis ihre Trennzeile ankommt. Ohne das springt eine Antwort bei jedem Chunk zwischen Rohtext und formatiertem Text hin und her. Wenn du bei react-markdown bleibst: Streamdown (opens in a new tab) beschreibt sich als Drop-in-Ersatz für KI-Streaming, und sein Parsing unvollständiger Blöcke (opens in a new tab) schließt offenes Fett, Inline-Code und Links. Der Guide zu React Markdown Streaming baut die Reparatur und das Memo pro Block Schritt für Schritt auf. - Eine Ansage pro Antwort. Der Thread ist nie eine Live-Region, denn ein Screenreader, der jedes gestreamte Wort vorliest, ist unbrauchbar. Eine separate Status-Region im Modus polite sagt an, wann eine Antwort beginnt und wann sie fertig ist, und die streamende Nachricht trägt
aria-busy.
Was muss der Composer abfangen?
Auf einer Hardware-Tastatur sendet Enter und Shift+Enter fügt eine Zeile ein. Enter sendet nie, solange eine IME komponiert. Touch-Tastaturen bekommen neue Zeilen, und der Senden-Button wird zu Stop, während die Antwort streamt.
Zuerst die IME, denn hier scheitern ganze Sprachen. Japanische, chinesische und koreanische Eingabemethoden bestätigen eine Umwandlung mit Enter. Die keydown-Referenz von MDN (opens in a new tab) weist darauf hin, dass isComposing beim keydown, der die IME schließt, false sein kann, während keyCode noch 229 ist. Der Guard prüft deshalb beides. Das ist die erste Zeile im Key-Handler von Prompt Input:
Der Rest:
- Touch-Tastaturen. Sie haben kein Shift+Enter, also fügt ein einfaches Enter auf dem Handy eine neue Zeile ein, und der runde Button sendet, ohne die Tastatur zu schließen. Der Text ist auf Handys 16px groß, damit iOS Safari nicht ins Feld zoomt.
- Fehlgeschlagenes Senden. Das Feld leert sich, sobald du sendest. Wenn
onSubmitein Promise zurückgibt, das rejected wird, kommen Text und Dateien zurück, sofern die Person nicht schon weitergetippt hat. - Stop. Mit
status="streaming"wird der Button zu Stop, undstopShortcut="escape"greift, solange der Fokus im Composer liegt. Ein Dialog auf der Seite behält so sein eigenes Escape. - Die Tastatur verdeckt das Feld. Seit Chrome 108 verkleinert Chrome auf Android beim Öffnen der Tastatur nur noch den Visual Viewport, genau wie Safari auf iOS (Ankündigung von Chrome (opens in a new tab)). Ein unten fixierter Composer landet dann dahinter. Mit
position="fixed"liest Prompt InputvisualViewportund hebt sich über einen Motion Value um die verdeckte Höhe an, ohne neu zu rendern. Innerhalb von Chat Thread brauchst du das nicht; der Thread pinnt den Composer mit Safe-Area-Padding. Mehr dazu: 100vh auf Mobile und Safe Area Insets in Tailwind (Englisch).
Der Guide zum React Prompt Input behandelt jeden dieser Punkte ausführlich, dazu Datei-Uploads mit Fortschritt und Diktat an der Cursor-Position.
Wie sollte Reasoning in einem KI-Chat aussehen?
Eingeklappt. Während das Modell denkt, zeigst du ein schimmerndes Label, einen laufenden Timer und die letzten zwei Zeilen des Gedankens. Wenn es fertig ist, bleibt „Thought for 12 sec“ stehen und öffnet sich per Tipp. Reasoning kann länger sein als die Antwort, und niemand sollte daran vorbeiscrollen müssen, um die Antwort zu finden.
Setz reasoning: { content, status, startedAt, endedAt } auf die Nachricht oder nutz Reasoning Indicator allein mit content, streaming und startedAt. Mit autoCollapse (standardmäßig an) schließt sich eine Zeile, die beim Start des Denkens offen war, sobald es endet. Eine Zeile, die die Person selbst geöffnet hat, bleibt offen. Auf dem Handy endet der geöffnete Inhalt bei 40% der Bildschirmhöhe, und die Zeile behält eine Trefferfläche von 44px.
$ npx wingo-ui@latest add reasoning-indicatorChat Message klappt Tool Calls genauso zu einer einzeiligen Zusammenfassung ein, die sich aufklappen lässt. Der Guide zur Tool Call UI für KI-Agenten zeigt die Status, Fehler und Freigabe-Zeilen, die jeder Aufruf braucht.
Wohin gehört der Model Picker auf dem Handy?
In den Composer, und er öffnet sich als Bottom Sheet. Auf einem 390px breiten Screen lässt ein 320px breites Popover an einem kleinen Toolbar-Button wenig Platz für Namen und Beschreibungen. Unter 768px wechselt Model Picker zu einem Sheet mit 64px hohen Zeilen und zweizeiligen Beschreibungen (responsive, standardmäßig an). Gib models an Prompt Input weiter, und der Picker wandert in dessen Toolbar:
Mit <PromptInput models={MODELS} defaultModel="standard" /> bekommt onSubmit die gewählte id als message.model. Gesperrte Modelle bleiben gedimmt in der Liste, mit disabledReason statt ihrer Beschreibung. So sehen Nutzer, was ein höherer Plan bringt, statt sich zu fragen, wo ein Modell geblieben ist. Bei mehr als 8 Modellen erscheint eine Suchzeile, und modelPickerProps={{ shortcut: "mod+." }} öffnet und schließt die Liste von überall, auch aus Textfeldern.
Wie verbindest du eine komplette KI-Chat-Seite mit deinem Modell?
Nimm den Block AI Chat Page und übergib onSend: einen async Generator, der Text-Deltas liefert, mit einem signal, das bei Stop, beim Wechsel des Gesprächs und beim Unmount abbricht. Stop behält, was angekommen ist, und markiert die Nachricht als gestoppt.
Die Props nach onSend sind wichtig. Ohne sie zeigt der Block seine Beispielinhalte: einen Verlauf mit zehn Gesprächen, Beispielmodelle und -tools, einen Nutzungsring, einen Plan-Upsell und einen Beispielnutzer. Im Playground ist das nützlich, in der Produktion falsch. Gib also deine eigenen Daten oder leere Werte mit. messages im Kontext enthält die neue Frage bereits.
Wirft der Handler einen Fehler, erscheint er an der Nachricht, zusammen mit einem Retry-Button. Ohne onSend läuft im Block ein geskripteter Mock mit Reasoning, Tool Calls und gestreamtem Markdown. So baust du den Screen, bevor das Backend existiert. Für eine React AI Chatbot UI in einem Support-Panel nimmst du Chat Thread allein mit variant="bubbles" und size="sm".
$ npx wingo-ui@latest add ai-chat-pageWann sind AI Elements oder assistant-ui die bessere Wahl?
Wenn deine App schon auf dem AI SDK läuft oder wenn die Bibliothek den Chat-State verwalten soll. Beide sind kostenlos und Open Source; die KI-Komponenten von Wingo UI sind kostenpflichtig und überlassen State und Transport dir. So schneiden die drei AI Chat UI Kits im Vergleich ab, Stand Oktober 2026:
Quellen: die Doku von AI Elements (opens in a new tab) und das Repository (opens in a new tab), die Primitives von assistant-ui (opens in a new tab), der Runtime-Guide (opens in a new tab) und das Repository (opens in a new tab) sowie unsere Preisseite.
Wenn deine App auf dem AI SDK läuft und du eine kostenlose shadcn Chat UI willst, fang mit AI Elements an. Wenn eine Bibliothek den Gesprächs-State verwalten soll, macht assistant-ui genau das. Wingo UI passt, wenn das Backend schon dir gehört und der Screen der Teil ist, den du nicht selbst bauen willst.
Was solltest du als Nächstes ausprobieren?
Öffne die Doku zur AI Chat Page, probier ihre Props im Playground aus und installier sie dann mit npx wingo-ui@latest add ai-chat-page. Sie und die fünf Komponenten aus diesem Guide gehören zu Wingo UI Pro. Weitere Guides zum Bauen mit Coding-Agenten findest du unter UI mit KI-Agenten bauen.
Häufige Fragen
Wie scrolle ich einen React-Chat automatisch mit, während die KI-Antwort streamt?
Folge neuem Inhalt nur, solange der Leser nahe am Ende ist, hör beim ersten Mausrad- oder Touch-Scroll nach oben auf und scroll wieder nach unten, wenn er eine Nachricht sendet oder einen Sprung-Button drückt. Schreib scrollTop in einer requestAnimationFrame-Schleife statt in React-State, damit das Streaming die Liste nicht neu rendert.
Warum sendet mein Chat-Eingabefeld die Nachricht, wenn ich in einer japanischen IME Enter drücke?
Das Enter, das die Umwandlung bestätigt, erreicht deinen keydown-Handler. Verlass den Handler sofort, wenn event.nativeEvent.isComposing true ist oder event.keyCode 229, denn laut MDN kann isComposing beim letzten keydown einer Komposition false sein.
Wie rendere ich streamendes Markdown in React ohne Flackern?
Repariere das unfertige Ende vor dem Parsen: schließ einen offenen Code-Fence und offene Hervorhebungen, verwirf einen halb getippten Link und halte eine Tabelle zurück, bis ihre Trennzeile da ist. Rendere die Antwort Block für Block, damit fertige Blöcke ihr DOM behalten und nur der letzte neu geparst wird.
Gibt es eine Chat-UI-Komponente für shadcn/ui?
shadcn/ui hat einen Message Scroller für den Verlauf, und AI Elements von Vercel ist eine kostenlose Komponentenbibliothek auf Basis von shadcn/ui für Apps mit dem AI SDK. Chat Thread, Prompt Input und die anderen KI-Komponenten von Wingo UI installierst du ebenfalls als Quelldateien, die dir gehören, und sie sind Teil von Wingo UI Pro.
Sind die KI-Chat-Komponenten von Wingo UI kostenlos?
Nein. Chat Thread, Chat Message, Prompt Input, Reasoning Indicator, Model Picker und der Block AI Chat Page gehören zu Wingo UI Pro. Die 75 kostenlosen Items enthalten Grundbausteine, auf denen sie aufbauen, etwa Button, Textarea und Tooltip.
- KI-Chat
- Streaming
- React
- Mobile UI
- Barrierefreiheit


