Sheet

A bottom sheet that animates

Sheet presents draggable content at configurable snap points and can host adaptive content from other overlay components.

Features

  • Lightweight implementation with drag support

  • Multiple snap points and a handle

  • Automatic screen-size adaptation

  • Animations, themes, and size props

Sheet is a bottom panel for mobile-friendly dialogs and action menus. It supports drag-to-dismiss, multiple snap points, and automatically stacks above other content.

Installation

Sheet is already installed in tamagui, or you can install it independently:

yarn add @tamagui/sheet

For native apps, we recommend setting up native portals to preserve React context inside Sheet content.

Anatomy

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Sheet.tsx (registry item "sheet")
// npm dependencies: @tamagui/core @tamagui/sheet
import { Sheet } from "../components/tamagui/Sheet"; // or '@tamagui/sheet'
export default () => <Sheet>
<Sheet.Overlay />
<Sheet.Handle />
<Sheet.Container>
<Sheet.Background />
{/* ...inner contents */}
</Sheet.Container>
</Sheet>;

API reference

Sheet

Contains every component for the sheet.

Props

  • open

    boolean

    Set to use as controlled component.

  • scope

    string

    Isolates this Sheet and its parts from other Sheet instances when needed.

  • 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

    (string | number)[]

    Default: 

    [80]

    Array of pixels or percents the sheet moves to when dragged. The first is the topmost and default when first opened. 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.

  • transitionConfig

    AnimatedNumberStrategy

    Customize the animation strategy for position transitions.

  • native

    boolean | "ios"[]

    (iOS only) Render with the system renderer registered through setupNativeSheet. UIKit does not expose continuous position, so Sheet.useAnimatedPosition() and onTransition are unavailable in this mode.

  • 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).

  • disableRemoveScroll

    boolean

    Default: 

    false

    Disables the RemoveScroll behavior that prevents body scrolling while sheet is open. By default, RemoveScroll is enabled when the sheet is open and modal.

  • portalProps

    PortalProps

    Props passed to the Portal that sheet uses when in modal mode.

  • moveOnKeyboardChange

    boolean

    Default: 

    false

    Makes the sheet move up when the mobile keyboard opens so the focused input remains visible. Works on native and on mobile web.

  • preferAdaptParentOpenState

    boolean

    Default: 

    false

    By default Sheet will prefer the open prop over a parent component that is controlling it via Adapt. In general if you want to Adapt to a sheet, you'd leave the open prop undefined. If you'd like to have the parent override the prop you've set manually on Sheet, set this to true.

  • onTransition

    (e: { phase: 'start' | 'end', cause: 'open' | 'close' | 'snap', position: number, finished?: boolean }) => void

    Fires at the start and end of the custom sheet's position transition. cause is "open" when moving from closed, "close" when moving off screen, and "snap" when moving between snap points while open. On the end phase, finished is false when the transition was interrupted (e.g. a close canceled by a re-open). position is the resolved translateY target in px from the top of the screen. Replaces the old onAnimationComplete prop. The native iOS system sheet does not expose this lifecycle.

  • disableHideWhenClosed

    boolean

    Default: 

    false

    By default a fully-closed sheet wrapper is hidden with display: "none". Set this to keep the closed wrapper laid out, for example so native visual effects can initialize below it. pointerEvents still gates interaction while closed.

  • unmountChildrenWhenHidden

    boolean

    Default: 

    false

    (experimental) Remove the children while the sheet is fully closed to save some rendering cost. Can interfere with animations.

  • Sheet.Overlay

    Displays behind the sheet content. Extends YStack.

    Sheet.Overlay must be a direct child of Sheet. It renders outside the animated content region so it stays fixed while the container moves.

    In v3 the sheet no longer fades anything for you. Sheet.Overlay has no baked-in opacity, so you drive its fade yourself. See Overlay fades for the two supported patterns.

    Sheet.Container

    Contains the sheet content and layout props. Extends YStack.

    Props

  • adjustPaddingForOffscreenContent

    boolean

    Adds padding for currently offscreen content so flex children size to the visible sheet area.

  • Sheet.Background

    The themed sheet surface. Extends YStack.

    Place it as the first child of Sheet.Container. By default it fills the container and extends past the bottom edge so spring overshoot does not reveal the page below the sheet. Put visual surface props like bg, borderRadius, and shadow* on Sheet.Background; keep layout props like padding, gap, and maxHeight on Sheet.Container.

    Props

  • disableHideBottomOverflow

    boolean

    Disables the default background extension below the sheet.

  • Sheet.Handle

    Shows a handle above the container by default. On tap, it will cycle between snapPoints, but this can be overridden with onPress.

    Extends XStack.

    In v3 the Handle ships no opacity of its own. The idle dim and the fade-in on open are aesthetics, so they live in the copied skin rather than the behavior package. Set opacity (and an open-driven opacity if you want it) on your own Sheet.Handle.

    Sheet.ScrollView

    Allows scrolling within Sheet. Extends ScrollView.

    Sheet.useAnimatedPosition

    Sheet.useAnimatedPosition() returns the sheet’s live animated position so you can drive effects that track the drag on the UI thread. Call it inside a Sheet; it throws with a clear message outside that scope.

    const { value, screenSize, frameSize, snapOffsets, minY } =
    Sheet.useAnimatedPosition()
    • value is the exact UniversalAnimatedNumber driving the frame’s translateY, in px from the top of the screen. Feed it to useAnimatedNumberStyle.
    • screenSize is the height of the viewport the sheet positions against.
    • frameSize is the measured height of the sheet frame.
    • snapOffsets are the resolved translateY positions, in the same order as snapPoints.
    • minY is the top-most (fully open) position.

    These are enough to compute any progress mapping inside a getStyle worklet. There is no separate derived progress value; the recipe lives in docs and the canonical skin.

    Sheet.useAnimatedPosition() and the Sheet onTransition callback are available on Tamagui’s custom Sheet. The native iOS system sheet selected by the native prop is driven by UIKit, which does not expose a continuous position. Remove the native prop when you need position-linked effects or transition lifecycle events.

    Overlay fades

    The sheet does not fade anything for you, so an overlay fade is your code. There are two supported patterns.

    1. Presence fade (animates on open and close only). Put enter/exit styles on Sheet.Overlay:

    <Sheet.Overlay transition="quick" opacity="0.5 enter:0 exit:0" />

    2. Drag-linked fade (tracks the finger). Read the animated position and map it to opacity with useAnimatedNumberStyle:

    // copy-paste: these skins import from files you own. copy each registry item
    // below into your app, then adjust the relative import paths to fit.
    // components/tamagui/Sheet.tsx (registry item "sheet")
    // npm dependencies: @tamagui/core @tamagui/sheet
    import type { Animated } from 'react-native';
    import { View as TamaguiView, useAnimationDriver, useAnimatedNumberStyle } from 'tamagui';
    // drag-linked overlay fade built entirely on the public hooks:
    // Sheet.useAnimatedPosition() gives the live translateY, useAnimatedNumberStyle
    // maps it to an opacity worklet. rendered through the driver's animated view so
    // it works on every driver (css re-renders + DOM transition, motion/reanimated
    // run on their own value).
    import { Sheet } from "../components/tamagui/Sheet";
    function DragLinkedBackdrop() {
    const animationDriver = useAnimationDriver();
    const AnimatedView = (animationDriver.View ?? TamaguiView) as typeof Animated.View;
    const {
    value,
    screenSize
    } = Sheet.useAnimatedPosition();
    const style = useAnimatedNumberStyle(value, (y: number) => {
    'worklet';
    return {
    opacity: Math.max(0, 0.6 * (1 - y / screenSize))
    };
    });
    return <AnimatedView transition="quick" style={[{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: '#000' }, style]} />;
    }
    <Sheet.Overlay backgroundColor="transparent">
    <DragLinkedBackdrop />
    </Sheet.Overlay>;

    Keep Sheet.Overlay as the direct child of Sheet, then render the hook-driven backdrop inside it. The driver’s animated view makes the worklet-driven style apply on every driver.

    Native system renderers

    Register a renderer at app startup to use system presentation with <Sheet native>. Without a registered renderer, Sheet uses its custom implementation. Tamagui owns open and position state, context, Adapt, and the lifetime of one content tree. The renderer owns platform presentation, authored detents, gestures, keyboard handling, and physical dismissal.

    import { setupNativeSheet, type NativeSheetRendererProps } from '@tamagui/sheet'
    setupNativeSheet('ios', NativeSheetRenderer)

    NativeSheetRendererProps supplies the resolved open value, onOpenChange, position, onPositionChange, and normalized snapPoints. A point is { type: 'percent', value: number }, { type: 'height', value: number }, or { type: 'fit' }. A dismissal-only bottom point is removed before forwarding. The other Sheet props, content, and host ref reach the renderer unchanged.

    Report interaction requests through onOpenChange and onPositionChange. Controlled parents may decline either request; continue presenting their accepted values. Call onDismiss only after the original presentation is physically removed following an accepted close. A reopen interrupts that completion. Tamagui then releases Adapt presence and, when requested, unmounts hidden children. Do not mount a second copy of the content beside the native presentation.

    Render children once inside the platform host. Sheet.Container measures content without custom sheet positioning, Sheet.ScrollView forwards to the native React Native ScrollView, and the system supplies the background, overlay, and grabber. Sheet.Background, Sheet.Overlay, and Sheet.Handle render nothing in this mode. Their styles and press handlers do not customize the system chrome. Use the renderer’s presentation callbacks for dismissal policy.

    The registration accepts a React component. An imperative implementation keeps a controller ref inside that component and drives its presentation methods from the resolved open value. Use a layout effect when these commands synchronize the host’s controlled value, so its acknowledgment observes the parent’s accepted state. Forward interaction requests and physical completion separately. Preserve the native library’s content container and lazy mounting behavior, and check parent-declined close and reopen during dismissal against its actual events. The previous setupNativeSheet('ios', modalModule) object registration is replaced by this component contract; no modal dependency is built into Tamagui.

    Native renderers decide which Sheet options their platform can represent. Reject unsupported combinations before presenting. A native iOS system sheet does not expose continuous animated position or custom transition events. moveOnKeyboardChange does not install Tamagui’s custom keyboard driver in a native renderer; its host handles the keyboard. Keep native off for custom sheet animation and gesture behavior.

    Native gesture handler integration

    For the best gesture experience on iOS and Android, Sheet supports optional integration with react-native-gesture-handler. This provides:

    • Smooth scroll-to-drag handoffs - transition between scrolling content and dragging the sheet
    • No gesture conflicts - Sheet and ScrollView gestures coordinate properly
    • Native-quality feel - matches the behavior of system sheets

    Setup

    1. Install react-native-gesture-handler:
    yarn add react-native-gesture-handler
    1. Add the setup import to your app entry point (before any Tamagui imports):
    // App.tsx or index.js
    import '@tamagui/native/setup-gesture-handler'
    import { GestureHandlerRootView } from 'react-native-gesture-handler'
    export default function App() {
    return (
    <GestureHandlerRootView style={{ flex: 1 }}>{/* Your app */}</GestureHandlerRootView>
    )
    }

    That’s it! Sheet will automatically detect and use the native gesture handler when available.

    Using Sheet.ScrollView

    When using scrollable content inside a Sheet, use Sheet.ScrollView for proper gesture coordination:

    <Sheet>
    <Sheet.Handle />
    <Sheet.Container>
    <Sheet.Background />
    <Sheet.ScrollView>{/* Scrollable content */}</Sheet.ScrollView>
    </Sheet.Container>
    </Sheet>

    This ensures:

    • Scrolling up at the top of content works naturally
    • Dragging down when scroll is at top drags the sheet
    • Direction changes mid-gesture work smoothly

    Without gesture handler

    If you don’t set up react-native-gesture-handler, Sheet falls back to React Native’s built-in PanResponder. This works well for basic use cases but has some limitations on iOS where scroll and pan gestures can occasionally conflict.

    Notes

    A fully-closed sheet is hidden with display: 'none' rather than by fading to transparent. Use disableHideWhenClosed if you need the closed wrapper to stay laid out. pointerEvents still gates interaction while closed, and unmountChildrenWhenHidden keys off the same closed state.

    For Android you need to manually re-propagate any context when using modal. This is because React Native doesn’t support portals yet.

    Source

    v2-look Sheet: styled handle (open/closed opacity), dimmed overlay, rounded background, padded container and scroll view, plus the controlled composition, over the unstyled @tamagui/ui Sheet behavior. This is the exact source the registry ships — copy it into your app and the examples above import from your copy.

    components/tamagui/Sheet.tsx
    // styled Sheet = the unstyled @tamagui/ui Sheet behavior primitive + the // default v2-look skin, layered here in `tamagui`. Single skin definition; the // shadcn registry item is generated from this file. import { createRefComponent, type GetProps, type GetRef, styled, withStaticProperties, } from '@tamagui/core' import { Sheet as SheetBehavior, type SheetProps, useSheetContext } from '@tamagui/sheet' const SheetHandleFrame = styled(SheetBehavior.Handle, { displayName: 'SheetHandle', height: 10, borderRadius: 1000, backgroundColor: 'color-5', zIndex: 10, marginHorizontal: '35%', marginBottom: '2', // Handle opacity aesthetics live in the skin, not the behavior package. opacity: '0.5 hover:0.7', variants: { open: { true: { opacity: 1, }, false: { opacity: 0, }, }, } as const, }) // the behavior forwards `open` to its inner handle frame, not to this styled // wrapper, so read the sheet context here and forward `open` — otherwise the // open-opacity variant above never toggles. export const SheetHandle = createRefComponent< GetRef<typeof SheetHandleFrame>, GetProps<typeof SheetHandleFrame> >(function SheetHandle(props, ref) { const context = useSheetContext((props as any).scope) return <SheetHandleFrame ref={ref} {...props} open={context.open} /> }) export const SheetOverlay = styled(SheetBehavior.Overlay, { displayName: 'SheetOverlay', backgroundColor: 'shadow-color', opacity: 0.45, }) export const SheetContainer = styled(SheetBehavior.Container, { displayName: 'SheetContainer', // no padding on the Container: the keyboard-avoidance measures this frame, and // in snapPointsMode="fit" any vertical padding here inflates the frame beyond // the preserved fit height, so the keyboard lift is off by exactly the padding // (SheetWebKeyboard geometry). Content inset comes from the ScrollView skin + // the consumer's own content padding, matching the pre-skin baseline. }) export const SheetBackground = styled(SheetBehavior.Background, { displayName: 'SheetBackground', backgroundColor: 'background', borderTopLeftRadius: '6', borderTopRightRadius: '6', }) export const SheetScrollView = styled(SheetBehavior.ScrollView, { displayName: 'SheetScrollView', // no flex here: the behavior's fitSizingStyle sets flex per mode (undefined for // snapPointsMode="fit" so the content-sized Container doesn't collapse, 1 // otherwise), and consumer props apply AFTER that guard — a skin-level flex:1 // would override it and collapse the fit-mode scrollview (SheetWebKeyboard). paddingHorizontal: '2', }) const sheetParts = { Container: SheetContainer, Background: SheetBackground, Overlay: SheetOverlay, Handle: SheetHandle, ScrollView: SheetScrollView, } type SheetRef = GetRef<typeof SheetBehavior.Root> export const SheetRoot = createRefComponent<SheetRef, SheetProps>( function SheetRoot(props, ref) { return <SheetBehavior.Root ref={ref} {...props} /> } ) const SheetControlledRoot = createRefComponent< SheetRef, Omit<SheetProps, 'open' | 'onOpenChange'> >(function SheetControlled(props, ref) { return <SheetBehavior.Controlled ref={ref} {...props} /> }) export const SheetControlled = withStaticProperties(SheetControlledRoot, sheetParts) export const Sheet = withStaticProperties(SheetRoot, { Root: SheetRoot, Controlled: SheetControlled, useAnimatedPosition: SheetBehavior.useAnimatedPosition, ...sheetParts, })

    Dependencies

    yarn add @tamagui/core @tamagui/sheet

    Expects theme tokens: background, color-5, shadow-color. Native: requires a Portal provider at the app root for the sheet to mount above content react-native-safe-area-context is used for safe-area insets on native (optional peer)

    Need raw behavior without any skin? tamagui/unstyled re-exports the @tamagui/ui primitives (advanced).