suiss/uim · React Native Component

Message Scroller

A chat-style message list that stays pinned to the newest message, tracks unread messages and loads older history.

Message Scroller is an open-source mobile component from suiss/uim for React Native and Expo, with SwiftUI and Jetpack Compose versions built from the same design tokens. The @esuiss/uim CLI copies its source into your project, so you own the code.

iOS
Message Scroller on iOS in light mode

MessageScroller is a View that hosts a scroll controller. MessageScrollerViewport renders your messages in an inverted FlatList, so the newest message sits at the bottom and new messages keep the list anchored there through maintainVisibleContentPosition. You pass data in chronological order; the viewport reverses it internally and maps indexes back for renderItem and keyExtractor.

MessageScrollerButton is a floating icon button that animates in with Reanimated when there is more content in its direction. When the user has scrolled up and new messages arrive, it shows an unread count badge.

Installation

$ pnpm dlx @esuiss/uim@latest add message-scroller

The CLI also adds what it builds on: Button, Icon, Text.

Manual

  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
  1. Add the pieces it builds on: Button, Icon, Text, and the primitives components/primitives/hooks.tsx.

  2. Copy the source into components/ui/message-scroller.tsx:

uniwind/components/ui/message-scroller.tsx
import { cn } from "cn"
import { ArrowDownIcon, ArrowUpIcon } from "phosphor-react-native"
import * as React from "react"
import {
  FlatList,
  type FlatListProps,
  Platform,
  View,
  type ViewProps,
  type ViewToken,
} from "react-native"
import {
  Easing,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  1. Update the import paths to match your project.

Usage

import { MessageScrollerProvider, MessageScroller, MessageScrollerViewport, MessageScrollerItem, MessageScrollerButton } from "@/components/ui/message-scroller"
<Chat initial={thread} />

Place MessageScrollerViewport and MessageScrollerButton inside MessageScroller. Wrap everything in MessageScrollerProvider when components outside the scroller, such as a composer, need to call the scroll hooks.

useMessageScroller returns scrollToEnd, scrollToStart and scrollToMessage(id, { align, animated }). useMessageScrollerScrollable, useMessageScrollerUnreadCount and useMessageScrollerVisibility read whether each edge is scrollable, the unread count, and the visible message ids.

Set onLoadOlder to fetch older history when the user scrolls near the top. Other FlatList props such as ListEmptyComponent pass through.

Examples

Unread while scrolled up

New messages arrive while scrolled up, so the button shows an unread count.

iOS
Message Scroller (unread while scrolled up) on iOS in light mode

Empty thread

An empty thread that renders an Empty state through ListEmptyComponent.

iOS
Message Scroller (empty thread) on iOS in light mode

Load older

Loads earlier messages with onLoadOlder when scrolling to the top.

iOS
Message Scroller (load older) on iOS in light mode

API Reference

MessageScrollerProvider

Shares one scroll controller with components outside the scroller.

ExtendsMessageScrollerOptions
PropType
Type
React.ReactNode
Default
—

MessageScroller

Container that clips the list and positions the button.

ExtendsViewProps

MessageScrollerViewport

Inverted FlatList that renders the messages.

ExtendsMessageScrollerViewportProps<T>

MessageScrollerItem

Row wrapper with horizontal padding.

ExtendsViewProps

MessageScrollerButton

Floating button that scrolls to the newest or oldest message.

ExtendsReact.ComponentProps<typeof Button>
PropType
Type
React.ReactNode
Default
—
About
end scrolls to the newest message, start to the oldest. Defaults to end.
Type
MessageScrollerScrollDirection
Default
"end"
About
Whether the scroll is animated. Defaults to true.
Type
boolean
Default
true

Accessibility

  • The button has a default label, "Scroll to end" or "Scroll to start", and includes the number of new messages when there are any.
  • While the button is hidden it ignores touches and is hidden from VoiceOver and TalkBack.
  • The button animation respects the system reduce motion setting.

Notes

  • The keyboard dismisses interactively on iOS and on drag on Android, and taps on handled children keep it open.
  • The button uses bg-surface-raised with border-separator, and the badge uses bg-accent with text-on-accent.
  • Light and dark colors follow the system appearance.