Expo UI and Router for iOSExpo SDK 57 and later, iOS only
Components

ScrollView

A SwiftUI ScrollView component for scrollable content.

Expo UI ScrollView matches the official SwiftUI ScrollView API and provides a scrollable container for its children.

Usage

Basic vertical scroll view

A simple vertically scrollable list of text items.

import { Host, ScrollView, VStack, Text } from '@expo/ui/swift-ui';
import { padding } from '@expo/ui/swift-ui/modifiers';

export default function ScrollViewVerticalExample() {
  return (
    <Host style={{ flex: 1 }}>
      <ScrollView>
        <VStack spacing={8}>
          {Array.from({ length: 30 }, (_, i) => (
            <Text key={i} modifiers={[padding({ horizontal: 16 })]}>
              {`Item ${i + 1}`}
            </Text>
          ))}
        </VStack>
      </ScrollView>
    </Host>
  );
}
A vertical scroll view listing items one to twenty-nine.
A vertical scroll view listing items one to twenty-nine.

Horizontal scroll view

Use the axes prop to scroll horizontally.

import {
  Host,
  ScrollView,
  HStack,
  RoundedRectangle,
} from '@expo/ui/swift-ui';
import {
  frame,
  foregroundStyle,
} from '@expo/ui/swift-ui/modifiers';

export default function ScrollViewHorizontalExample() {
  return (
    <Host style={{ flex: 1 }}>
      <ScrollView axes="horizontal">
        <HStack spacing={8}>
          {Array.from({ length: 20 }, (_, i) => (
            <RoundedRectangle
              key={i}
              cornerRadius={12}
              modifiers={[
                frame({ width: 100, height: 100 }),
                foregroundStyle(`hsl(${i * 18}, 70%, 50%)`),
              ]}
            />
          ))}
        </HStack>
      </ScrollView>
    </Host>
  );
}
A horizontal row of rounded squares shading from red through orange to yellow.
A horizontal row of rounded squares shading from red through orange to yellow.

Hidden scroll indicators

Set showsIndicators to false to hide the scroll bars.

import { Host, ScrollView, VStack, Text } from '@expo/ui/swift-ui';

export default function ScrollViewHiddenIndicatorsExample() {
  return (
    <Host style={{ flex: 1 }}>
      <ScrollView showsIndicators={false}>
        <VStack spacing={8}>
          {Array.from({ length: 30 }, (_, i) => (
            <Text key={i}>{`Item ${i + 1}`}</Text>
          ))}
        </VStack>
      </ScrollView>
    </Host>
  );
}

Shared scroll position

Requires iOS 17 or later. On older versions, the modifier is a no-op.

Track the leading scroll target id from JavaScript and scroll to a target by writing to the state.

  • Mark each scroll target with the id modifier, wrap the content container in scrollTargetLayout, and apply the scrollPosition modifier to the ScrollView.
  • The optional onChange callback fires on the JS thread when the leading target changes.

The scrollPosition modifier also works on other scrollable containers like LazyVStack and LazyHStack.

Writes to state.value must run on the UI runtime. Wrap the write in scheduleOnUI from react-native-worklets, or call them from inside a 'worklet' function. Writes from the JS runtime trip Main Thread Checker, Xcode's runtime tool that flags UIKit calls made from a background thread.

import {
  Button,
  Host,
  ScrollView,
  Text,
  VStack,
  useNativeState,
} from '@expo/ui/swift-ui';
import {
  id,
  padding,
  scrollPosition,
  scrollTargetLayout,
} from '@expo/ui/swift-ui/modifiers';
import { scheduleOnUI } from 'react-native-worklets';

export default function ScrollViewSharedPositionExample() {
  const activeID = useNativeState<string | null>(null);

  return (
    <Host style={{ flex: 1 }}>
      <VStack spacing={12}>
        <ScrollView
          modifiers={[
            scrollPosition(activeID, {
              onChange: newID => {
                console.log('[JS thread] leading target:', newID);
              },
            }),
          ]}>
          <VStack modifiers={[scrollTargetLayout()]}>
            {Array.from({ length: 30 }, (_, i) => (
              <Text
                key={`item-${i}`}
                modifiers={[
                  id(`item-${i}`),
                  padding({ horizontal: 16, vertical: 12 }),
                ]}>
                {`Item ${i}`}
              </Text>
            ))}
          </VStack>
        </ScrollView>
        <Button
          label="Scroll to item 10 from worklet"
          onPress={() => {
            scheduleOnUI(() => {
              'worklet';
              activeID.value = 'item-10';
            });
          }}
        />
      </VStack>
    </Host>
  );
}
A scrollable list of items above a button labelled Scroll to item 10 from worklet.
A scrollable list of items above a button labelled Scroll to item 10 from worklet.

API

import { ScrollView } from '@expo/ui/swift-ui';

Component

ScrollView

Type: React.Element<ScrollViewProps>

SwiftUI ScrollView wrapper.

  • To control scroll position, pair this with the scrollPosition(state, { onChange }) modifier and a useNativeState-backed id.
  • Write state.value = targetId for an instant scroll, or wrap the write in withAnimation(...) from @expo/ui/swift-ui for an animated one.

ScrollViewProps

axes

Optional • Literal type: string • Default: 'vertical'

The scrollable axes. Pass 'both' to enable 2D (horizontal + vertical) scrolling.

Acceptable values are: 'vertical' | 'horizontal' | 'both'

children

Type: ReactNode

showsIndicators

Optional • Type: boolean • Default: true

Whether to show scroll indicators. For richer visibility control (e.g. 'never') or per-axis control, use the scrollIndicators(...) modifier instead.

Inherited props

Types

ScrollGeometry

Snapshot of a ScrollView's scroll geometry, emitted by the useScrollGeometryChange(...) and onScrollPhaseChange(...) modifiers (iOS 18+).

PropertyTypeDescription
containerHeightnumberHeight of the visible scroll container, in points.
containerWidthnumberWidth of the visible scroll container, in points.
contentHeightnumberTotal height of the scrollable content, in points.
contentOffsetXnumberHorizontal content offset, in points.
contentOffsetYnumberVertical content offset, in points.
contentWidthnumberTotal width of the scrollable content, in points.

ScrollPhase

Literal type: string

Scroll phase emitted by the onScrollPhaseChange(...) modifier. Mirrors SwiftUI's ScrollPhase (iOS 18+).

Acceptable values are: 'idle' | 'tracking' | 'interacting' | 'animating' | 'decelerating'

Modifiers

Use these modifiers in the modifiers prop of the SwiftUI and Universal version.

Expo UI lets you put any of the 89 modifiers for any view on ScrollView, but not all of them do something here.

  • 74 change what you see on ScrollView.
  • 2 work, but not in the way that you may expect.
  • 2 have no visible effect on ScrollView.
  • 13 work only in one place, such as a widget or a Grid.

Best modifiers for ScrollView

These modifiers are made for ScrollView, or they are the ones that you use most with it.

Also works on ScrollView

ScrollView takes these modifiers too, and each one works in the usual way.

Needs care on ScrollView

These modifiers work on ScrollView, but not in the way that you may expect.

ModifierWhat to know
onLongPressGesturePut the handler on a child view. A gesture on the scroll view itself can stop the scroll.
onTapGesturePut the handler on a child view. A gesture on the scroll view itself can stop the scroll.

No visible effect on ScrollView

ScrollView accepts these modifiers, but you see no change. They do not give an error.

ModifierWhy nothing happens
layoutPriorityIt has an effect only when the view shares an axis with other views in a stack.
zIndexIt changes the order of views that cover each other in the same parent, and ScrollView does not stack its children.

Only in some places

These modifiers work only in a widget, in a Grid, in a Namespace or in a parent that asks for them.

ModifierWhere it works
activityBackgroundTintWorks only in a Live Activity. It has no effect in the app.
alignmentGuideHas an effect only when the parent, such as a VStack or ZStack, aligns its children by the same guide.
containerBackgroundWorks only in a widget, or in a navigation container when you use a navigation placement.
glassEffectIdWorks only on a view that also has glassEffect, in a GlassEffectContainer and a Namespace.
gridCellAnchor iOS 16+Works only on a cell of a Grid. Grid is in @expo/ui/swift-ui, but it has no Expo docs page.
gridCellColumnsWorks only on a view in a Grid.Row. Grid is in @expo/ui/swift-ui, but it has no Expo docs page.
gridCellUnsizedAxesWorks only on a view in a Grid. Grid is in @expo/ui/swift-ui, but it has no Expo docs page.
gridColumnAlignment iOS 16+Works only on a view in a Grid.Row. Grid is in @expo/ui/swift-ui, but it has no Expo docs page.
invalidatableContent iOS 17+Has an effect only when a parent view uses redacted with the invalidated reason.
matchedGeometryEffectWorks only in a Namespace. Give two views the same id and change between them with an animation.
privacySensitiveHas an effect only when a parent view uses redacted with the privacy reason.
unredactedHas an effect only in a parent view that uses redacted.
widgetURLWorks only in a widget. It has no effect in the app.

Sources

The original Expo docs for this page:

On this page