Sheet
A bottom sheet that slides up.
import { ChevronDown, ChevronUp } from '@tamagui/lucide-icons'import type { SheetProps } from '@tamagui/sheet'import { Sheet } from '@tamagui/sheet'import React from 'react'import { Button, H2, Input, Paragraph, XStack, YStack } from 'tamagui'const spModes = ['percent', 'constant', 'fit', 'mixed'] as constexport const SheetDemo = () => {const [position, setPosition] = React.useState(0)const [open, setOpen] = React.useState(false)const [modal, setModal] = React.useState(true)const [innerOpen, setInnerOpen] = React.useState(false)const [snapPointsMode, setSnapPointsMode] =React.useState<(typeof spModes)[number]>('percent')const [mixedFitDemo, setMixedFitDemo] = React.useState(false)const isPercent = snapPointsMode === 'percent'const isConstant = snapPointsMode === 'constant'const isFit = snapPointsMode === 'fit'const isMixed = snapPointsMode === 'mixed'const snapPoints = isPercent? [85, 50, 25]: isConstant? [256, 190]: isFit? undefined: mixedFitDemo? ['fit', 110]: ['80%', 256, 190]return (<><YStack gap="$4"><XStack gap="$4" $sm={{ flexDirection: 'column', alignItems: 'center' }}><Button onPress={() => setOpen(true)}>Open</Button><Button onPress={() => setModal((x) => !x)}>{modal ? 'Type: Modal' : 'Type: Inline'}</Button><Button onPress={() => setSnapPointsMode( (prev) => spModes[(spModes.indexOf(prev) + 1) % spModes.length] ) } >{`Mode: ${ { percent: 'Percentage', constant: 'Constant', fit: 'Fit', mixed: 'Mixed' }[ snapPointsMode ] }`}</Button></XStack>{isMixed ? (<Button onPress={() => setMixedFitDemo((x) => !x)}>{`Snap Points: ${JSON.stringify(snapPoints)}`}</Button>) : (<XStack paddingVertical="$2.5" justifyContent="center"><Paragraph>{`Snap Points: ${isFit ? '(none)' : JSON.stringify(snapPoints)}`}</Paragraph></XStack>)}</YStack><Sheet forceRemoveScrollEnabled={open} modal={modal} open={open} onOpenChange={setOpen} snapPoints={snapPoints} snapPointsMode={snapPointsMode} dismissOnSnapToBottom position={position} onPositionChange={setPosition} zIndex={100_000} animation="medium" ><Sheet.Overlay animation="lazy" enterStyle={{ opacity: 0 }} exitStyle={{ opacity: 0 }} /><Sheet.Handle /><Sheet.Frame padding="$4" justifyContent="center" alignItems="center" gap="$5"><Button size="$6" circular icon={ChevronDown} onPress={() => setOpen(false)} /><Input width={200} />{modal && isPercent && (<><InnerSheet open={innerOpen} onOpenChange={setInnerOpen} /><Button size="$6" circular icon={ChevronUp} onPress={() => setInnerOpen(true)} /></>)}</Sheet.Frame></Sheet></>)}function InnerSheet(props: SheetProps) {return (<Sheet animation="medium" modal snapPoints={[90]} dismissOnSnapToBottom {...props}><Sheet.Overlay animation="medium" enterStyle={{ opacity: 0 }} exitStyle={{ opacity: 0 }} /><Sheet.Handle /><Sheet.Frame flex={1} justifyContent="center" alignItems="center" gap="$5"><Sheet.ScrollView><YStack p="$5" gap="$8"><Button size="$6" circular alignSelf="center" icon={ChevronDown} onPress={() => props.onOpenChange?.(false)} /><H2>Hello world</H2>{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (<Paragraph key={i} size="$8">Eu officia sunt ipsum nisi dolore labore est laborum laborum in esse adpariatur. Dolor excepteur esse deserunt voluptate labore ea. Exercitationipsum deserunt occaecat cupidatat consequat est adipisicing velitcupidatat ullamco veniam aliquip reprehenderit officia. Officia laboreculpa ullamco velit. In sit occaecat velit ipsum fugiat esse aliqua dolorsint.</Paragraph>))}</YStack></Sheet.ScrollView></Sheet.Frame></Sheet>)}
Features
Lightweight implementation with dragging support.
Multiple snap point points and a handle.
Automatically adjusts to screen size.
Accepts animations, themes, size props and more.
Anatomy
import { Sheet } from 'tamagui' // or '@tamagui/sheet'export default () => (<Sheet><Sheet.Overlay /><Sheet.Handle /><Sheet.Frame>{/* ...inner contents */}</Sheet.Frame></Sheet>)
API
<Sheet />
Contains every component for the sheet.
Props
open
boolean
Set to use as controlled component.
defaultOpen
boolean
Uncontrolled open state on mount.
onOpenChange
(open: boolean) => void
Called on change open, controlled or uncontrolled.
position
number
Controlled position, set to an index of snapPoints.
defaultPosition
number
Uncontrolled default position on mount.
snapPoints
number[]
Default:
[80, 10]
Array of numbers, 0-100 that corresponds to % of the screen it should take up. Should go from most visible to least visible in order. Use "open" prop for fully closed.
onPositionChange
(position: number) => void
Called on change position, controlled or uncontrolled.
dismissOnOverlayPress
boolean
Default:
true
Controls tapping on the overlay to close, defaults to true.
animationConfig
Animated.SpringAnimationConfig
Default:
true
Customize the spring used, passed to react-native Animated.spring().
disableDrag
boolean
Disables all touch events to drag the sheet.
modal
boolean
Renders sheet into the root of your app instead of inline.
dismissOnSnapToBottom
boolean
Adds a snap point to the end of your snap points set to "0", that when snapped to will set open to false (uncontrolled) and call onOpenChange with false (controlled).
forceRemoveScrollEnabled
boolean
Default:
false
By default. Tamagui uses react-remove-scroll to prevent anything outside the sheet scrolling. This can cause some issues so you can override the behavior with this prop (either true or false).
portalProps
Object
YStack props that can be passed to the Portal that sheet uses when in modal mode.
moveOnKeyboardChange
boolean
Default:
false
Native-only flag that will make the sheet move up when the mobile keyboard opens so the focused input remains visible.
<Overlay />
Displays behind Frame. Extends YStack.
<Frame />
Contains the content. Extends YStack.
<Handle />
Shows a handle above the frame by default, on tap it will cycle between snapPoints
but this can be overridden with onPress
.
Extends XStack.
<Scrollview />
Allows scrolling within Sheet. Extends Scrollview.
Notes
For Android you need to manually re-propagate any context when using modal
. This is because React Native doesn't support portals yet.