---
modificationDate: February 14, 2026
title: BottomSheet
description: A SwiftUI BottomSheet 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: ['ios', 'tvos']
---

<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/swift-ui/bottomsheet/","feedback":"🤖 Agent feedback: <specific, actionable description>"}'

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

</AgentInstructions>

# BottomSheet

A SwiftUI BottomSheet component that presents content from the bottom of the screen.
iOS, tvOS

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

Expo UI BottomSheet matches the official SwiftUI [sheet API](https://developer.apple.com/documentation/swiftui/view/sheet\(ispresented:ondismiss:content:\)) and presents content from the bottom of the screen.

## 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

```tsx
import { useState } from 'react';
import { Host, BottomSheet, Button, Text, VStack } from '@expo/ui/swift-ui';

export default function BasicBottomSheetExample() {
  const [isPresented, setIsPresented] = useState(false);

  return (
    <Host style={{ flex: 1 }}>
      <VStack>
        <Button label="Open Sheet" onPress={() => setIsPresented(true)} />
        <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented}>
          <Text>Hello, world!</Text>
        </BottomSheet>
      </VStack>
    </Host>
  );
}
```

### Bottom sheet that fits content

Use the [`fitToContents`](/versions/v55.0.0/sdk/ui/swift-ui/bottomsheet#fittocontents) prop to automatically size the sheet to fit its content.

```tsx
import { useState } from 'react';
import { Host, BottomSheet, Button, Text, VStack } from '@expo/ui/swift-ui';

export default function BottomSheetFitsContentExample() {
  const [isPresented, setIsPresented] = useState(false);

  return (
    <Host style={{ flex: 1 }}>
      <VStack>
        <Button label="Open Sheet" onPress={() => setIsPresented(true)} />
        <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} fitToContents>
          <VStack>
            <Text>This sheet automatically sizes to fit its content.</Text>
            <Button label="Close" onPress={() => setIsPresented(false)} />
          </VStack>
        </BottomSheet>
      </VStack>
    </Host>
  );
}
```

### Bottom sheet with presentation detents

Use the [`presentationDetents`](/versions/v55.0.0/sdk/ui/swift-ui/modifiers#presentationdetentsdetents-options) modifier on `Group` to control the available heights. You can use:

-   `'medium'`: System medium height (approximately half screen)
-   `'large'`: System large height (full screen)
-   `{ fraction: number }`: Fraction of screen height (0-1)
-   `{ height: number }`: Fixed height in points

```tsx
import { useState } from 'react';
import { Host, BottomSheet, Button, Text, VStack } from '@expo/ui/swift-ui';
import { presentationDetents } from '@expo/ui/swift-ui/modifiers';

export default function BottomSheetWithDetentsExample() {
  const [isPresented, setIsPresented] = useState(false);

  return (
    <Host style={{ flex: 1 }}>
      <VStack>
        <Button label="Open Sheet" onPress={() => setIsPresented(true)} />
        <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented}>
          <Group
            modifiers={[
              presentationDetents(['medium', 'large', { fraction: 0.3 }, { height: 200 }]),
            ]}>
            <Text>This sheet can snap to multiple heights.</Text>
          </Group>
        </BottomSheet>
      </VStack>
    </Host>
  );
}
```

### Bottom sheet with detent selection tracking

Pass `selection` and `onSelectionChange` options to [`presentationDetents`](/versions/v55.0.0/sdk/ui/swift-ui/modifiers#presentationdetentsdetents-options) to programmatically control which detent the sheet snaps to.

```tsx
import { useState } from 'react';
import { Host, BottomSheet, Button, List, Section, Text, VStack, Group } from '@expo/ui/swift-ui';
import {
  presentationDetents,
  presentationDragIndicator,
  foregroundStyle,
} from '@expo/ui/swift-ui/modifiers';
import type { PresentationDetent } from '@expo/ui/swift-ui/modifiers';

export default function BottomSheetWithDetentSelectionExample() {
  const [isPresented, setIsPresented] = useState(false);
  const detents: PresentationDetent[] = [{ height: 300 }, { fraction: 0.3 }, 'medium', 'large'];
  const [selectedDetent, setSelectedDetent] = useState<PresentationDetent>('medium');

  const formatDetent = (detent: PresentationDetent): string => {
    if (typeof detent === 'string') return detent;
    if ('fraction' in detent) return `Fraction ${detent.fraction}`;
    return `Height ${detent.height}`;
  };

  return (
    <Host style={{ flex: 1 }}>
      <VStack>
        <Button label="Show Sheet" onPress={() => setIsPresented(true)} />
        <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented}>
          <Group
            modifiers={[
              presentationDetents(detents, {
                selection: selectedDetent,
                onSelectionChange: setSelectedDetent,
              }),
              presentationDragIndicator('visible'),
            ]}>
            <List>
              <Section title="Change Detent">
                <Button label="Height 300" onPress={() => setSelectedDetent({ height: 300 })} />
                <Button label="Fraction 0.3" onPress={() => setSelectedDetent({ fraction: 0.3 })} />
                <Button label="Medium" onPress={() => setSelectedDetent('medium')} />
                <Button label="Large" onPress={() => setSelectedDetent('large')} />
              </Section>
              <Section title="Current">
                <Text modifiers={[foregroundStyle('secondaryLabel')]}>
                  {formatDetent(selectedDetent)}
                </Text>
              </Section>
            </List>
          </Group>
        </BottomSheet>
      </VStack>
    </Host>
  );
}
```

### Bottom sheet with background interaction

Use the [`presentationBackgroundInteraction`](/versions/v55.0.0/sdk/ui/swift-ui/modifiers#presentationbackgroundinteractioninteraction) modifier to allow interactions with content behind the sheet.

```tsx
import { useState } from 'react';
import { Host, BottomSheet, Button, Text, VStack } from '@expo/ui/swift-ui';
import {
  presentationDetents,
  presentationBackgroundInteraction,
} from '@expo/ui/swift-ui/modifiers';

export default function BottomSheetWithBackgroundInteractionExample() {
  const [isPresented, setIsPresented] = useState(false);

  return (
    <Host style={{ flex: 1 }}>
      <VStack>
        <Button label="Open Sheet" onPress={() => setIsPresented(true)} />
        <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented}>
          <Group
            modifiers={[
              presentationDetents(['medium', 'large']),
              presentationBackgroundInteraction({ type: 'enabledUpThrough', detent: 'medium' }),
            ]}>
            <Text>Interact with content behind when at medium height.</Text>
          </Group>
        </BottomSheet>
      </VStack>
    </Host>
  );
}
```

### Non-dismissible bottom sheet

Use the [`interactiveDismissDisabled`](/versions/v55.0.0/sdk/ui/swift-ui/modifiers#interactivedismissdisabledisdisabled) modifier to prevent users from dismissing the sheet by swiping.

```tsx
import { useState } from 'react';
import { Host, BottomSheet, Button, Text, VStack } from '@expo/ui/swift-ui';
import { interactiveDismissDisabled } from '@expo/ui/swift-ui/modifiers';

export default function NonDismissibleBottomSheetExample() {
  const [isPresented, setIsPresented] = useState(false);

  return (
    <Host style={{ flex: 1 }}>
      <VStack>
        <Button label="Open Sheet" onPress={() => setIsPresented(true)} />
        <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented}>
          <Group modifiers={[interactiveDismissDisabled()]}>
            <VStack>
              <Text>This sheet cannot be dismissed by swiping.</Text>
              <Button label="Close" onPress={() => setIsPresented(false)} />
            </VStack>
          </Group>
        </BottomSheet>
      </VStack>
    </Host>
  );
}
```

### Bottom sheet with React Native content

Use `RNHostView` to embed React Native components inside the bottom sheet. Set `matchContents` to automatically size the host view to fit its content.

```tsx
import { useState } from 'react';
import { Pressable, Text as RNText, View } from 'react-native';
import { Host, BottomSheet, Button, RNHostView, VStack } from '@expo/ui/swift-ui';
import { presentationDragIndicator } from '@expo/ui/swift-ui/modifiers';

export default function BottomSheetWithRNContentExample() {
  const [isPresented, setIsPresented] = useState(false);
  const [counter, setCounter] = useState(0);

  return (
    <Host style={{ flex: 1 }}>
      <VStack>
        <Button label="Open Sheet" onPress={() => setIsPresented(true)} />
        <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} fitToContents>
          <Group modifiers={[presentationDragIndicator('visible')]}>
            <RNHostView matchContents>
              <View style={{ padding: 24 }}>
                <RNText style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 8 }}>
                  React Native Content
                </RNText>
                <RNText style={{ color: '#666', marginBottom: 16 }}>Counter: {counter}</RNText>
                <Pressable
                  style={{
                    backgroundColor: '#007AFF',
                    padding: 12,
                    borderRadius: 8,
                    alignItems: 'center',
                    marginBottom: 12,
                  }}
                  onPress={() => setCounter(counter + 1)}>
                  <RNText style={{ color: 'white', fontWeight: '600' }}>Increment</RNText>
                </Pressable>
                <Pressable
                  style={{
                    backgroundColor: '#FF3B30',
                    padding: 12,
                    borderRadius: 8,
                    alignItems: 'center',
                  }}
                  onPress={() => setIsPresented(false)}>
                  <RNText style={{ color: 'white', fontWeight: '600' }}>Close</RNText>
                </Pressable>
              </View>
            </RNHostView>
          </Group>
        </BottomSheet>
      </VStack>
    </Host>
  );
}
```

### Bottom sheet with flexible React Native content

When using React Native content with `flex: 1`, omit the `matchContents` prop on `RNHostView` and use [`presentationDetents`](/versions/v55.0.0/sdk/ui/swift-ui/modifiers#presentationdetentsdetents-options) to control the sheet height.

```tsx
import { useState } from 'react';
import { Text as RNText, View } from 'react-native';
import { Host, BottomSheet, Button, RNHostView, VStack } from '@expo/ui/swift-ui';
import { presentationDetents, presentationDragIndicator } from '@expo/ui/swift-ui/modifiers';

export default function BottomSheetWithFlexRNContentExample() {
  const [isPresented, setIsPresented] = useState(false);

  return (
    <Host style={{ flex: 1 }}>
      <VStack>
        <Button label="Open Sheet" onPress={() => setIsPresented(true)} />
        <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented}>
          <Group
            modifiers={[
              presentationDetents(['medium', 'large']),
              presentationDragIndicator('visible'),
            ]}>
            <RNHostView>
              <View style={{ flex: 1, backgroundColor: '#007AFF', padding: 24 }}>
                <RNText style={{ fontSize: 18, fontWeight: 'bold', color: 'white' }}>
                  Flexible React Native Content
                </RNText>
                <RNText style={{ color: 'white', marginTop: 8 }}>
                  This content fills the available space in the sheet.
                </RNText>
              </View>
            </RNHostView>
          </Group>
        </BottomSheet>
      </VStack>
    </Host>
  );
}
```

## API

```tsx
import { BottomSheet } from '@expo/ui/swift-ui';
```

## Component

### `BottomSheet`

Supported platforms: iOS, tvOS.

Type: React.[Element](https://www.typescriptlang.org/docs/handbook/jsx.html#function-component)<[BottomSheetProps](#bottomsheetprops)\>

`BottomSheet` presents content from the bottom of the screen.

BottomSheetProps

### `children`

Supported platforms: iOS, tvOS.

Type: `React.ReactNode`

The children of the `BottomSheet` component. Use `Group` to wrap your content and apply presentation modifiers like `presentationDetents`, `presentationDragIndicator`, `presentationBackgroundInteraction`, and `interactiveDismissDisabled`.

### `fitToContents`

Supported platforms: iOS, tvOS.

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

When `true`, the sheet will automatically size itself to fit its content. This sets the presentation detent to match the height of the children.

### `isPresented`

Supported platforms: iOS, tvOS.

Type: `boolean`

Whether the `BottomSheet` is presented.

### `onIsPresentedChange`

Supported platforms: iOS, tvOS.

Type: `(isPresented: boolean) => void`

Callback function that is called when the `BottomSheet` presented state changes.

#### Inherited Props

-   [CommonViewModifierProps](/versions/v55.0.0/sdk/ui/swift-ui/modifiers)
