{"slug": "chat-uis-are-lists-until-they-aren-t", "title": "Chat UIs Are Lists Until They Aren't", "summary": "TanStack Virtual has released a new feature enabling reverse infinite scroll for chat interfaces, addressing the unique scrolling behaviors required by modern chat UIs. The update introduces `anchorTo: 'end'` and `followOnAppend` properties that automatically keep the viewport pinned to the latest messages when users are at the bottom, while preventing incoming messages from disrupting users who have scrolled up to read history. This eliminates the need for developers to manually manage scroll math, column-reverse CSS, or inverted transforms when building chat applications.", "body_md": "*by Tanner Linsley on May 25, 2026.*\n\nIn the last TanStack Virtual release, I left one thing on the table: reverse infinite scroll for chat, and it deserved its own pass.\n\nChat used to be a niche UI, now it's everywhere, in support inboxes, activity logs, multiplayer feeds, copilots, AI agents, and streaming assistants. They all look like lists, but they don't behave like the lists virtualization libraries were originally built around.\n\nA normal virtual list is start-anchored, so the top of the content is the stable point. You scroll down, append more rows, measure dynamic heights, and everything mostly works.\n\nChat flips that contract.\n\nThat last part matters. If someone scrolls up to read history, incoming messages shouldn't yank them back to the bottom, and if they're already there, the UI should stay pinned without every app rewriting the same scroll math.\n\nTanStack Virtual now has a first-class way to model that.\n\n``` js\nconst virtualizer = useVirtualizer({\n  count: messages.length,\n  getScrollElement: () => parentRef.current,\n  estimateSize: () => 72,\n  getItemKey: (index) => messages[index]!.id,\n  anchorTo: 'end',\n  followOnAppend: true,\n  scrollEndThreshold: 80,\n})\njs\nconst virtualizer = useVirtualizer({\n  count: messages.length,\n  getScrollElement: () => parentRef.current,\n  estimateSize: () => 72,\n  getItemKey: (index) => messages[index]!.id,\n  anchorTo: 'end',\n  followOnAppend: true,\n  scrollEndThreshold: 80,\n})\n```\n\nanchorTo: 'end' tells the virtualizer that the end of the list is the edge you want to preserve.\n\nWhen you prepend older messages, TanStack Virtual captures the currently visible item, finds the same keyed item after the data changes, and adjusts the scroll offset so it stays in the same visual position.\n\nThat means no column-reverse, no inverted transforms, and no manual scrollTop += delta bookkeeping in every app. Just normal data:\n\n``` js\nsetMessages((current) => [...olderMessages, ...current])\njs\nsetMessages((current) => [...olderMessages, ...current])\n```\n\nThe only real requirement is a stable key:\n\n``` js\ngetItemKey: (index) => messages[index]!.id\njs\ngetItemKey: (index) => messages[index]!.id\n```\n\nIndex keys can't make prepend stability work, because after a prepend every old item moves to a new index, and the virtualizer needs to know which message is still the same message.\n\nfollowOnAppend handles the \"stay at latest, unless I am reading history\" rule.\n\nIf the user is already near the end, appended messages keep the viewport pinned, and if they've scrolled up, new output lands below without stealing their place.\n\n```\nfollowOnAppend: true\nfollowOnAppend: true\n```\n\nYou can also pass a scroll behavior:\n\n```\nfollowOnAppend: 'smooth'\nfollowOnAppend: 'smooth'\n```\n\nThe threshold is configurable too:\n\n```\nscrollEndThreshold: 80\nscrollEndThreshold: 80\n```\n\nThat same end-state logic is exposed for UI:\n\n```\nvirtualizer.isAtEnd()\nvirtualizer.getDistanceFromEnd()\nvirtualizer.scrollToEnd()\nvirtualizer.isAtEnd()\nvirtualizer.getDistanceFromEnd()\nvirtualizer.scrollToEnd()\n```\n\nSo your \"Jump to latest\" button can use the same rules as the virtualizer itself.\n\nThe modern version of chat isn't append-a-message, it's append a message and then resize it dozens or hundreds of times while tokens stream in.\n\nWithout end anchoring, the scroll height grows but the scroll offset doesn't, so the user slowly drifts away from the bottom.\n\nWith anchorTo: 'end', if the viewport was pinned before the last item grew, TanStack Virtual applies the size delta and keeps the end pinned.\n\nThat's the point of this feature: the common chat behaviors aren't userland chores anymore.\n\nThis still isn't a chat component.\n\nTanStack Virtual still doesn't render bubbles, loaders, timestamps, avatars, unread dividers, or composer UI. That part belongs to your app.\n\nWhat it does now is handle the scroll physics that almost every chat UI needs:\n\nIt's a small API with a pretty big ergonomic win.\n\nThere is also a new [Chat guide](/virtual/latest/docs/chat) and a [React chat example](/virtual/latest/docs/framework/react/examples/chat) showing history prepends, appended messages, streaming replies, and a \"Latest\" control built with scrollToEnd().\n\nChat is one of the dominant UI patterns of modern apps now, and TanStack Virtual should make it feel boring to build.", "url": "https://wpnews.pro/news/chat-uis-are-lists-until-they-aren-t", "canonical_source": "https://tanstack.com/blog/tanstack-virtual-chat", "published_at": "2026-05-27 07:12:35+00:00", "updated_at": "2026-05-27 07:27:21.508509+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "ai-agents"], "entities": ["Tanner Linsley", "TanStack Virtual"], "alternates": {"html": "https://wpnews.pro/news/chat-uis-are-lists-until-they-aren-t", "markdown": "https://wpnews.pro/news/chat-uis-are-lists-until-they-aren-t.md", "text": "https://wpnews.pro/news/chat-uis-are-lists-until-they-aren-t.txt", "jsonld": "https://wpnews.pro/news/chat-uis-are-lists-until-they-aren-t.jsonld"}}