SplitView

Two stacked panes with a drag row that settles on named snap points.

Open Source

Installation

SplitView ships with the library — install it once, then import:

import { SplitView } from "@delacour/react-native-ui/split-view";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add split-view

Usage

app/index.tsx
import { SplitView } from "@/components/ui/split-view";
import { Text } from "@/components/ui/text";
import { View } from "react-native";

const RESULTS = ["Harbour Café", "Kelburn Park", "Cuba Street", "Te Papa"];

export default function Example() {
  return (
    <SplitView className="flex-1" snapPoints={[0.3, 0.6, 1]} defaultSnapIndex={1}>
      <SplitView.Top>
        <View className="flex-1 items-center justify-center bg-muted">
          <Text.Label>Map</Text.Label>
        </View>
      </SplitView.Top>
      <SplitView.DragArea accessibilityLabel="Resize map" />
      <SplitView.Bottom>
        <View className="gap-3 p-4">
          {RESULTS.map((name) => (
            <Text.Paragraph key={name}>{name}</Text.Paragraph>
          ))}
        </View>
      </SplitView.Bottom>
    </SplitView>
  );
}

The root needs an extent

Stops are resolved against the box, so give the root flex-1 or a fixed height. Until it has been measured both panes share it equally.

Drag the row

The top pane follows the finger and settles on the nearest stop, or the next one along when flicked; a tap on the grip cycles.

Anatomy

Snap points

A stop at or below 1 is a fraction of the box, less the 44pt drag row; above 1 it is points. The two can be mixed, and the stops are ordered by the height they resolve to.

Snap points
<SplitView className="flex-1" snapPoints={[120, 0.5, 1]} defaultSnapIndex={0}>
  <SplitView.Top />
  <SplitView.DragArea />
  <SplitView.Bottom />
</SplitView>

minTopHeight and minBottomHeight are floors in points. A stop below the top floor is lifted to it; a stop that would crush the bottom pane is lowered, so 1 with minBottomHeight={100} leaves the bottom pane its hundred points.

Prefer one unit unless a stop has to be a fixed size. A mixed list whose order differs between portrait and landscape keeps its index across a rotation, so the same index can name a different stop.

Variants

panes, the default, gives each pane a rounded card surface with the drag row between them. seam leaves the panes flush and draws a hairline behind the grip, for a split that already sits on a card or a sheet.

Seam
<Card className="overflow-hidden p-0">
  <SplitView className="h-96" variant="seam">
    <SplitView.Top>
      <Chart />
    </SplitView.Top>
    <SplitView.DragArea accessibilityLabel="Resize chart" />
    <SplitView.Bottom>
      <Table />
    </SplitView.Bottom>
  </SplitView>
</Card>

Controlled index

snapIndex and onSnapIndexChange own the stop. onSnapIndexChange fires once per settle, never per frame. useSplitView() gives a child the same index and snapTo, so a toolbar inside a pane can move the divider.

Controlled
function Expand() {
  const { snapCount, snapTo } = useSplitView();
  return (
    <Button onPress={() => snapTo(snapCount - 1)}>
      <Button.Label>Expand map</Button.Label>
    </Button>
  );
}

const [index, setIndex] = useState(1);

<SplitView className="flex-1" snapIndex={index} onSnapIndexChange={setIndex}>
  <SplitView.Top>
    <Expand />
  </SplitView.Top>
  <SplitView.DragArea />
  <SplitView.Bottom />
</SplitView>

Scrolling content

The gesture lives on the drag row only. A list in either pane scrolls without moving the divider, and a drag that starts on the row wins over the list. Scrollables inside a pane are yours to add.

Scrolling panes
<SplitView className="flex-1" minTopHeight={80} minBottomHeight={120}>
  <SplitView.Top>
    <Document />
  </SplitView.Top>
  <SplitView.DragArea accessibilityLabel="Resize comments" />
  <SplitView.Bottom>
    <ScrollView>{comments}</ScrollView>
  </SplitView.Bottom>
</SplitView>

Accessibility

The drag row is adjustable. A screen reader reads its label and position — "Resize map, Position 2 of 3". Swiping up or down steps one stop, and the step is only offered where there is a stop to step to. A double tap cycles and announces where it landed. The grip is hidden from assistive technology.

Under Reduce Motion the drag still follows the finger, but a release jumps to its stop rather than springing.

SplitView, Splitter or BottomSheet

SplitViewSplitterBottomSheet
Panesexactly twotwo or morecontent over the app
Axisverticaleithervertical
Releasea named stopwhere it was let goa snap point, or dismissed
ValuesnapIndexlayout, in percentthe sheet's index
Backdrop, dismissal, keyboardnonoyes

Both panes of a SplitView are always on screen and nothing is dismissed. Use Splitter for free resizing or more than two panes, and BottomSheet for content that floats over the screen.

Tablets and landscape

SplitView never switches to side by side by itself. Deciding that is up to the app: on a wide window, render a horizontal Splitter instead.

import { useWindowDimensions } from "react-native";

function Results() {
  const { width } = useWindowDimensions();

  if (width >= 768) {
    return (
      <Splitter className="flex-1" defaultLayout={[60, 40]}>
        <Splitter.Pane minSize={40}>
          <ResultsMap />
        </Splitter.Pane>
        <Splitter.Handle accessibilityLabel="Resize map" />
        <Splitter.Pane minSize={30}>
          <ResultsList />
        </Splitter.Pane>
      </Splitter>
    );
  }

  return (
    <SplitView className="flex-1">
      <SplitView.Top>
        <ResultsMap />
      </SplitView.Top>
      <SplitView.DragArea accessibilityLabel="Resize map" />
      <SplitView.Bottom>
        <ResultsList />
      </SplitView.Bottom>
    </SplitView>
  );
}

API Reference

Anatomy

PartWhat it is
SplitViewResolves the stops, owns the top pane's height and the settle
SplitView.TopThe upper pane, whose height moves
SplitView.DragAreaThe 44pt row that drags, taps to cycle and is the adjustable element
SplitView.BottomThe lower pane, filling what is left
SplitView.HandleThe grip capsule, drawn by DragArea when it has no children

SplitView

Prop

Type

SplitView.Top and SplitView.Bottom

Prop

Type

SplitView.DragArea

Prop

Type

SplitView.Handle

Prop

Type

useSplitView

Prop

Type

On this page