---
modificationDate: March 17, 2026
title: ModalBottomSheet
description: A Jetpack Compose ModalBottomSheet component that presents content from the bottom of the screen.
sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-ui'
packageName: '@expo/ui'
platforms: ['android']
---

<AgentInstructions>

## Submitting Feedback

If this page contains errors, outdated information, or gaps that blocked you from completing a task, report it so the docs can be improved:

curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/versions/v55.0.0/sdk/ui/jetpack-compose/bottomsheet/","feedback":"🤖 Agent feedback: <specific, actionable description>"}'

Only submit when you have something specific and actionable to report.

</AgentInstructions>

# ModalBottomSheet

A Jetpack Compose ModalBottomSheet component that presents content from the bottom of the screen.
Android

> For the complete documentation index, see [llms.txt](/llms.txt). Use this file to discover all available pages.

Expo UI ModalBottomSheet matches the official Jetpack Compose [Bottom Sheet API](https://developer.android.com/develop/ui/compose/components/bottom-sheets) and displays content in a modal sheet that slides up from the bottom.

## Installation

```sh
npx expo install @expo/ui
```

If you are installing this in an [existing React Native app](/bare/overview), make sure to [install `expo`](/bare/installing-expo-modules) in your project.

## Usage

### Basic bottom sheet

Use `ref.hide()` to programmatically dismiss the sheet with an animation before unmounting it.

```tsx
import { useRef, useState } from 'react';
import { Host, ModalBottomSheet, Button, Column, Text } from '@expo/ui/jetpack-compose';
import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose';
import { paddingAll } from '@expo/ui/jetpack-compose/modifiers';

export default function BasicBottomSheetExample() {
  const [visible, setVisible] = useState(false);
  const sheetRef = useRef<ModalBottomSheetRef>(null);

  const hideSheet = async () => {
    await sheetRef.current?.hide();
    setVisible(false);
  };

  return (
    <Host matchContents>
      <Button onClick={() => setVisible(true)}>
        <Text>Open Sheet</Text>
      </Button>
      {visible && (
        <ModalBottomSheet ref={sheetRef} onDismissRequest={() => setVisible(false)}>
          <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[paddingAll(24)]}>
            <Text>Hello from bottom sheet!</Text>
            <Text>You can add more content here.</Text>
            <Button onClick={hideSheet}>
              <Text>Close</Text>
            </Button>
          </Column>
        </ModalBottomSheet>
      )}
    </Host>
  );
}
```

### Skip partially expanded state

When `skipPartiallyExpanded` is set, the sheet opens directly in the fully expanded state instead of stopping at the half-height position first.

```tsx
import { useRef, useState } from 'react';
import { Host, ModalBottomSheet, Button, Column, Text } from '@expo/ui/jetpack-compose';
import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose';
import { paddingAll } from '@expo/ui/jetpack-compose/modifiers';

export default function SkipPartiallyExpandedExample() {
  const [visible, setVisible] = useState(false);
  const sheetRef = useRef<ModalBottomSheetRef>(null);

  const hideSheet = async () => {
    await sheetRef.current?.hide();
    setVisible(false);
  };

  return (
    <Host matchContents>
      <Button onClick={() => setVisible(true)}>
        <Text>Open Sheet</Text>
      </Button>
      {visible && (
        <ModalBottomSheet
          ref={sheetRef}
          onDismissRequest={() => setVisible(false)}
          skipPartiallyExpanded>
          <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[paddingAll(24)]}>
            <Text>This sheet skips the partially expanded state.</Text>
            <Text>It opens directly in the fully expanded position.</Text>
            <Button onClick={hideSheet}>
              <Text>Close</Text>
            </Button>
          </Column>
        </ModalBottomSheet>
      )}
    </Host>
  );
}
```

### Custom colors

Use `containerColor`, `contentColor`, and `scrimColor` to customize the sheet's appearance.

```tsx
import { useRef, useState } from 'react';
import { Host, ModalBottomSheet, Button, Column, Text } from '@expo/ui/jetpack-compose';
import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose';
import { paddingAll } from '@expo/ui/jetpack-compose/modifiers';

export default function CustomColorsExample() {
  const [visible, setVisible] = useState(false);
  const sheetRef = useRef<ModalBottomSheetRef>(null);

  const hideSheet = async () => {
    await sheetRef.current?.hide();
    setVisible(false);
  };

  return (
    <Host matchContents>
      <Button onClick={() => setVisible(true)}>
        <Text>Open Colored Sheet</Text>
      </Button>
      {visible && (
        <ModalBottomSheet
          ref={sheetRef}
          onDismissRequest={() => setVisible(false)}
          containerColor="#1a1a2e"
          contentColor="#e0e0e0"
          scrimColor="#806200EE">
          <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[paddingAll(24)]}>
            <Text>Custom styled bottom sheet.</Text>
            <Text>Dark container with a purple scrim overlay.</Text>
            <Button onClick={hideSheet}>
              <Text>Close</Text>
            </Button>
          </Column>
        </ModalBottomSheet>
      )}
    </Host>
  );
}
```

### Custom drag handle

Use `ModalBottomSheet.DragHandle` slot to provide a custom drag handle, or set `showDragHandle={false}` to hide it entirely.

```tsx
import { useRef, useState } from 'react';
import { Host, ModalBottomSheet, Button, Column, Box, Text } from '@expo/ui/jetpack-compose';
import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose';
import {
  background,
  clip,
  fillMaxWidth,
  height,
  padding,
  Shapes,
  width,
} from '@expo/ui/jetpack-compose/modifiers';

export default function CustomDragHandleExample() {
  const [visible, setVisible] = useState(false);
  const sheetRef = useRef<ModalBottomSheetRef>(null);

  const hideSheet = async () => {
    await sheetRef.current?.hide();
    setVisible(false);
  };

  return (
    <Host matchContents>
      <Button onClick={() => setVisible(true)}>
        <Text>Open Custom Handle Sheet</Text>
      </Button>
      {visible && (
        <ModalBottomSheet ref={sheetRef} onDismissRequest={() => setVisible(false)}>
          <ModalBottomSheet.DragHandle>
            <Column horizontalAlignment="center" modifiers={[fillMaxWidth(), padding(0, 12, 0, 8)]}>
              <Box modifiers={[width(60), height(6), clip(Shapes.Circle), background('#6200EE')]} />
            </Column>
          </ModalBottomSheet.DragHandle>
          <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[padding(16, 16, 16, 16)]}>
            <Button onClick={hideSheet}>
              <Text>Close</Text>
            </Button>
          </Column>
        </ModalBottomSheet>
      )}
    </Host>
  );
}
```

### React Native content inside a bottom sheet

Use `RNHostView` to embed interactive React Native views inside a Compose bottom sheet. This lets you mix Compose layout with RN components like `Pressable` and `Text`.

```tsx
import { useRef, useState } from 'react';
import { Host, ModalBottomSheet, Button, Column, RNHostView, Text } from '@expo/ui/jetpack-compose';
import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose';
import { padding } from '@expo/ui/jetpack-compose/modifiers';
import { Pressable, Text as RNText, View } from 'react-native';

export default function RNContentBottomSheetExample() {
  const [visible, setVisible] = useState(false);
  const sheetRef = useRef<ModalBottomSheetRef>(null);

  const hideSheet = async () => {
    await sheetRef.current?.hide();
    setVisible(false);
  };

  return (
    <Host matchContents>
      <Button onClick={() => setVisible(true)}>
        <Text>Open RN Content Sheet</Text>
      </Button>
      {visible && (
        <ModalBottomSheet
          ref={sheetRef}
          onDismissRequest={() => setVisible(false)}
          skipPartiallyExpanded={false}>
          <Column verticalArrangement={{ spacedBy: 16 }} modifiers={[padding(16, 16, 16, 16)]}>
            <Text>Mixing Compose + RN in a Bottom Sheet</Text>
            <RNHostView matchContents>
              <View>
                <RNText style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 8 }}>
                  React Native Content
                </RNText>
                <Pressable
                  style={{
                    backgroundColor: '#007AFF',
                    padding: 12,
                    borderRadius: 8,
                    alignItems: 'center',
                  }}
                  onPress={hideSheet}>
                  <RNText style={{ color: 'white', fontWeight: '600' }}>Close</RNText>
                </Pressable>
              </View>
            </RNHostView>
          </Column>
        </ModalBottomSheet>
      )}
    </Host>
  );
}
```

### React Native content with flex

Use `RNHostView` without `matchContents` to let the RN view fill the remaining space inside the sheet. Combine with a fixed `height` modifier on the parent `Column` to control the sheet size.

```tsx
import { useRef, useState } from 'react';
import { Host, ModalBottomSheet, Button, Column, RNHostView, Text } from '@expo/ui/jetpack-compose';
import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose';
import { height, padding } from '@expo/ui/jetpack-compose/modifiers';
import { Text as RNText, View } from 'react-native';

export default function FlexRNContentExample() {
  const [visible, setVisible] = useState(false);
  const sheetRef = useRef<ModalBottomSheetRef>(null);

  const hideSheet = async () => {
    await sheetRef.current?.hide();
    setVisible(false);
  };

  return (
    <Host matchContents>
      <Button onClick={() => setVisible(true)}>
        <Text>Open Flex Content Sheet</Text>
      </Button>
      {visible && (
        <ModalBottomSheet
          ref={sheetRef}
          onDismissRequest={() => setVisible(false)}
          skipPartiallyExpanded>
          <Column modifiers={[height(400), padding(16, 16, 16, 16)]}>
            <Text>RN View with flex: 1</Text>
            <RNHostView>
              <View style={{ flex: 1, backgroundColor: '#9B59B6', borderRadius: 10 }}>
                <RNText
                  style={{
                    color: 'white',
                    fontSize: 18,
                    fontWeight: 'bold',
                    padding: 16,
                  }}>
                  React Native Content (flex: 1)
                </RNText>
              </View>
            </RNHostView>
          </Column>
        </ModalBottomSheet>
      )}
    </Host>
  );
}
```

### Non-dismissible sheet

Combine `properties`, `sheetGesturesEnabled` to create a sheet that can only be closed programmatically.

```tsx
import { useRef, useState } from 'react';
import { Host, ModalBottomSheet, Button, Column, Text } from '@expo/ui/jetpack-compose';
import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose';
import { paddingAll } from '@expo/ui/jetpack-compose/modifiers';

export default function NonDismissibleExample() {
  const [visible, setVisible] = useState(false);
  const sheetRef = useRef<ModalBottomSheetRef>(null);

  const hideSheet = async () => {
    await sheetRef.current?.hide();
    setVisible(false);
  };

  return (
    <Host matchContents>
      <Button onClick={() => setVisible(true)}>
        <Text>Open Non-Dismissible Sheet</Text>
      </Button>
      {visible && (
        <ModalBottomSheet
          ref={sheetRef}
          onDismissRequest={() => setVisible(false)}
          sheetGesturesEnabled={false}
          properties={{
            shouldDismissOnBackPress: false,
            shouldDismissOnClickOutside: false,
          }}>
          <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[paddingAll(24)]}>
            <Text>This sheet cannot be dismissed by swiping, back press, or tapping outside.</Text>
            <Text>Only the button below will close it.</Text>
            <Button onClick={hideSheet}>
              <Text>Close</Text>
            </Button>
          </Column>
        </ModalBottomSheet>
      )}
    </Host>
  );
}
```

## API

```tsx
import { ModalBottomSheet } from '@expo/ui/jetpack-compose';
```

## Constants

### `BottomSheet.ModalBottomSheet`

Supported platforms: Android.

Type: `ModalBottomSheetComponent`

## Props

### `children`

Supported platforms: Android.

Type: `React.ReactNode`

The children of the `ModalBottomSheet` component. Can include a `ModalBottomSheet.DragHandle` slot for a custom drag handle.

### `containerColor`

Supported platforms: Android.

Optional • Type: [ColorValue](https://reactnative.dev/docs/colors)

The background color of the bottom sheet.

### `contentColor`

Supported platforms: Android.

Optional • Type: [ColorValue](https://reactnative.dev/docs/colors)

The preferred color of the content inside the bottom sheet.

### `modifiers`

Supported platforms: Android.

Optional • Type: `ModifierConfig[]`

Modifiers for the component.

### `onDismissRequest`

Supported platforms: Android.

Type: `() => void`

Callback function that is called when the user dismisses the bottom sheet (via swipe, back press, or tapping outside the scrim).

### `properties`

Supported platforms: Android.

Optional • Type: [ModalBottomSheetProperties](#modalbottomsheetproperties)

Properties for the modal window behavior.

### `ref`

Supported platforms: Android.

Optional • Type: Ref<[ModalBottomSheetRef](#modalbottomsheetref)\>

Can be used to imperatively hide the bottom sheet with an animation.

### `scrimColor`

Supported platforms: Android.

Optional • Type: [ColorValue](https://reactnative.dev/docs/colors)

The color of the scrim overlay behind the bottom sheet.

### `sheetGesturesEnabled`

Supported platforms: Android.

Optional • Type: `boolean` • Default: `true`

Whether gestures (swipe to dismiss) are enabled on the bottom sheet.

### `showDragHandle`

Supported platforms: Android.

Optional • Type: `boolean` • Default: `true`

Whether to show the default drag handle at the top of the bottom sheet. Ignored if a custom `ModalBottomSheet.DragHandle` slot is provided.

### `skipPartiallyExpanded`

Supported platforms: Android.

Optional • Type: `boolean` • Default: `false`

Immediately opens the bottom sheet in full screen.

## Types

### `ModalBottomSheetProperties`

Supported platforms: Android.

| Property | Type | Description |
| --- | --- | --- |
| shouldDismissOnBackPress(optional) | `boolean` | Whether the bottom sheet can be dismissed by pressing the back button. Default: `true` |
| shouldDismissOnClickOutside(optional) | `boolean` | Whether the bottom sheet can be dismissed by clicking outside (on the scrim). Default: `true` |

### `ModalBottomSheetRef`

Supported platforms: Android.

| Property | Type | Description |
| --- | --- | --- |
| hide | () => [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<void\> | Programmatically hides the bottom sheet with an animation. The returned promise resolves after the dismiss animation completes. |
