Components

Sheet

Sheet is a panel that slides up from the bottom of the screen over a dimmed backdrop. The user closes it by dragging it down, tapping the backdrop or pressing the Android back button. The drag and the animations run natively: the panel follows the finger on the UI thread, with no JS and no render per frame.

ui-sheet.tsx Edit in Playground Open in new tab
// Sheet: a bottom panel dragged down, tapped outside or closed with a button.
import { mount, signal, Text, View } from "nsh";
import { Button, Sheet } from "nsh/ui/mobile";

function App() {
  const open = signal(false);
  const closes = signal(0);
  const opens = signal(0);
  return (
    <View style={{ flex: 1, backgroundColor: "#ffffff", padding: 16, paddingTop: 24, gap: 12 }}>
      <Text style={{ fontSize: 22, fontWeight: "700" }}>Sheet</Text>
      <Text style={{ color: "#737373" }}>{`Opened ${opens()} times, closed by the user ${closes()} times`}</Text>
      <Button title="Open the sheet" onPress={() => {
          open.set(true);
          opens.set(opens() + 1);
        }}
      />
      <Sheet
        open={open()}
        onClose={() => {
          open.set(false);
          closes.set(closes() + 1);
        }}
      >
        <Text style={{ fontSize: 17, fontWeight: "600" }}>Share the photo</Text>
        <Text style={{ color: "#737373" }}>Drag the panel down, tap the dimmed backdrop or press Done.</Text>
        <Button title="Copy link" variant="secondary" onPress={() => open.set(false)} />
        <Button title="Done" onPress={() => open.set(false)} />
      </Sheet>
    </View>
  );
}

mount(App);
import { Sheet } from "nsh/ui/mobile";

#Usage

Sheet is controlled. It is shown while open is true. When the user closes it, onClose is called, and you set open to false:

import { mount, signal, Text, View } from "nsh";
import { Button, Sheet } from "nsh/ui/mobile";

function App() {
  const open = signal(false);
  return (
    <View style={{ flex: 1, padding: 16, paddingTop: 48 }}>
      <Button title="Share" onPress={() => open.set(true)} />
      <Sheet open={open()} onClose={() => open.set(false)}>
        <Text style={{ fontSize: 17, fontWeight: "600" }}>Share the photo</Text>
        <Button title="Copy link" variant="secondary" onPress={() => open.set(false)} />
        <Button title="Done" onPress={() => open.set(false)} />
      </Sheet>
    </View>
  );
}

mount(App);

#Behavior

  • Opening: when open becomes true, the panel slides in from below the screen with a spring, and the backdrop fades in as the panel rises.
  • Dragging: the panel follows a vertical drag. When the user releases it more than 120 pt below its resting position, or flicks it down faster than 900 pt/s, it closes. Otherwise it springs back, continuing at the speed of the flick.
  • Closing by the user (drag, backdrop tap, Android back button): the panel slides down in 220 ms, then onClose is called. Always set open to false in onClose: while open stays true, the sheet’s modal stays presented after the panel has slid out of view, and it keeps covering the screen.
  • Closing from your code: setting open to false removes the sheet at once, without the slide-down animation, and does not call onClose.

The sheet is presented in a transparent modal, so it covers everything, including a navigation header and a tab bar. It can be declared anywhere in the tree.

#Layout

The panel is as tall as its content, and the content is stacked in a column with 12 pt gaps. The panel has a grabber at the top, 16 pt horizontal padding, rounded top corners, and bottom padding of the safe area’s bottom inset plus 16 pt, so the content stays above the home indicator.

For a list in a sheet, give the list’s parent a height:

<Sheet open={picking()} onClose={() => picking.set(false)}>
  <Text style={{ fontSize: 17, fontWeight: "600" }}>Choose a city</Text>
  <View style={{ height: 320 }}>
    <SelectList
      options={cities}
      value={city()}
      onSelect={(i) => {
        city.set(i);
        picking.set(false);
      }}
    />
  </View>
</Sheet>

#Props

PropTypeDefaultDescription
openbooleanfalseWhether the sheet is shown.
childrenJSX.ElementnoneThe panel’s content.
class, classList, style, testID, onLayout, refApplied to the sheet’s host view in your tree, not to the panel.

#Events

EventPayloadWhen
onClosenoneThe user closed the sheet (drag, backdrop tap or back button), after it slid away. Set open to false.

#Theming

PartTheme key
Panelbackground
Backdropoverlay
Grabbermuted.background
Top cornersradius.lg, at least 16 pt

See Theming. The default overlay color is light (5% black in light mode), so the backdrop is only slightly dimmed. Set overlay in your theme for a darker backdrop.

#Accessibility

The backdrop is labeled “Close”, so screen reader users can close the sheet by activating it. On Android, the back button closes the sheet.