Introduction
Components
- Accordion
- Action Sheet
- Alert
- Audio Player
- Audio Recorder
- Audio Waveform
- Avatar
- Badge
- BottomSheet
- Button
- Camera
- Camera Preview
- Card
- Carousel
- Checkbox
- Collapsible
- Color Picker
- Combobox
- Date Picker
- File Picker
- Gallery
- Hello Wave
- Icon
- Image
- Input
- Input OTP
- Link
- MediaPicker
- Mode Toggle
- Onboarding
- ParallaxScrollView
- Picker
- Popover
- Progress
- Radio
- ScrollView
- SearchBar
- Separator
- Share
- Sheet
- Skeleton
- Spinner
- Switch
- Table
- Tabs
- Text
- Toast
- Toggle
- Video
- View
Charts
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';
export function BottomSheetDemo() {
const { isVisible, open, close } = useBottomSheet();
return (
<View>
<Button onPress={open}>Open Bottom Sheet</Button>
<BottomSheet
isVisible={isVisible}
onClose={close}
snapPoints={[0.3, 0.6, 0.9]}
>
<View style={{ gap: 16 }}>
<Text variant='title'>Welcome to Bottom Sheet</Text>
<Text>
This is a basic bottom sheet that supports gesture interactions. You
can drag it up and down to different snap points, or swipe down
quickly to dismiss it.
</Text>
<Button onPress={close}>Close</Button>
</View>
</BottomSheet>
</View>
);
}
Installation
pnpm dlx bna-ui add bottom-sheet
Usage
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
function MyComponent() {
const { isVisible, open, close } = useBottomSheet();
return (
<>
<Button onPress={open}>Open Bottom Sheet</Button>
<BottomSheet
isVisible={isVisible}
onClose={close}
title='Settings'
snapPoints={[0.3, 0.6, 0.9]}
>
<Text>Your content here</Text>
</BottomSheet>
</>
);
}
Examples
Default
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';
export function BottomSheetDemo() {
const { isVisible, open, close } = useBottomSheet();
return (
<View>
<Button onPress={open}>Open Bottom Sheet</Button>
<BottomSheet
isVisible={isVisible}
onClose={close}
snapPoints={[0.3, 0.6, 0.9]}
>
<View style={{ gap: 16 }}>
<Text variant='title'>Welcome to Bottom Sheet</Text>
<Text>
This is a basic bottom sheet that supports gesture interactions. You
can drag it up and down to different snap points, or swipe down
quickly to dismiss it.
</Text>
<Button onPress={close}>Close</Button>
</View>
</BottomSheet>
</View>
);
}
With Title
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';
export function BottomSheetTitle() {
const { isVisible, open, close } = useBottomSheet();
return (
<View>
<Button onPress={open}>Open Sheet with Title</Button>
<BottomSheet
isVisible={isVisible}
onClose={close}
title='Settings'
snapPoints={[0.4, 0.7]}
>
<View style={{ gap: 16 }}>
<Text>
This bottom sheet includes a title in the header area. The title is
centered and uses the theme's title text style.
</Text>
<Button variant='secondary' onPress={close}>
Done
</Button>
</View>
</BottomSheet>
</View>
);
}
Custom Snap Points
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';
export function BottomSheetSnapPoints() {
const { isVisible, open, close } = useBottomSheet();
return (
<View>
<Button onPress={open}>Custom Snap Points</Button>
<BottomSheet
isVisible={isVisible}
onClose={close}
title='Custom Heights'
snapPoints={[0.2, 0.5, 0.8, 0.95]}
>
<View style={{ gap: 16 }}>
<Text variant='title'>Multiple Snap Points</Text>
<Text>
This sheet has four different snap points: 20%, 50%, 80%, and 95% of
screen height. Try dragging to see how it snaps to each position.
</Text>
<View style={{ gap: 12 }}>
<Text variant='body'>Available heights:</Text>
<Text>β’ 20% - Peek view</Text>
<Text>β’ 50% - Medium height</Text>
<Text>β’ 80% - Large view</Text>
<Text>β’ 95% - Nearly fullscreen</Text>
</View>
</View>
</BottomSheet>
</View>
);
}
Form Content
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
export function BottomSheetForm() {
const { isVisible, open, close } = useBottomSheet();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = () => {
// Handle form submission
console.log('Form submitted:', { name, email });
close();
};
return (
<View>
<Button onPress={open}>Edit Profile</Button>
<BottomSheet
isVisible={isVisible}
onClose={close}
title='Edit Profile'
snapPoints={[0.6, 0.8]}
enableBackdropDismiss={false}
>
<View style={{ gap: 20 }}>
<View style={{ gap: 12 }}>
<Text variant='body'>Name</Text>
<Input
value={name}
onChangeText={setName}
variant='outline'
placeholder='Enter your name'
/>
</View>
<View style={{ gap: 12 }}>
<Text variant='body'>Email</Text>
<Input
value={email}
onChangeText={setEmail}
variant='outline'
placeholder='Enter your email'
keyboardType='email-address'
/>
</View>
<View
style={{
flex: 1,
width: '100%',
flexDirection: 'row',
gap: 12,
marginTop: 12,
}}
>
<Button variant='outline' onPress={close} style={{ flex: 1 }}>
Cancel
</Button>
<Button onPress={handleSubmit} style={{ flex: 2 }}>
Save
</Button>
</View>
</View>
</BottomSheet>
</View>
);
}
List Content
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';
import { FlatList, TouchableOpacity } from 'react-native';
const items = [
{ id: '1', title: 'Photos', subtitle: '1,234 items' },
{ id: '2', title: 'Videos', subtitle: '56 items' },
{ id: '3', title: 'Documents', subtitle: '89 items' },
{ id: '4', title: 'Audio', subtitle: '23 items' },
{ id: '5', title: 'Downloads', subtitle: '12 items' },
{ id: '6', title: 'Archives', subtitle: '4 items' },
];
export function BottomSheetList() {
const { isVisible, open, close } = useBottomSheet();
const renderItem = ({ item }: { item: (typeof items)[0] }) => (
<TouchableOpacity
style={{
padding: 16,
borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.1)',
}}
onPress={() => console.log('Selected:', item.title)}
>
<Text variant='body' style={{ fontWeight: '600' }}>
{item.title}
</Text>
<Text variant='caption' style={{ marginTop: 2 }}>
{item.subtitle}
</Text>
</TouchableOpacity>
);
return (
<View>
<Button onPress={open}>Browse Files</Button>
<BottomSheet
isVisible={isVisible}
onClose={close}
title='File Browser'
snapPoints={[0.5, 0.8]}
>
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
/>
</BottomSheet>
</View>
);
}
No Backdrop Dismiss
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';
export function BottomSheetNoDismiss() {
const { isVisible, open, close } = useBottomSheet();
return (
<View>
<Button onPress={open}>Important Action</Button>
<BottomSheet
isVisible={isVisible}
onClose={close}
title='Confirm Action'
snapPoints={[0.4]}
enableBackdropDismiss={false}
>
<View style={{ gap: 16 }}>
<Text>
This bottom sheet cannot be dismissed by tapping the backdrop. You
must use one of the action buttons below.
</Text>
<Text variant='caption' style={{ fontStyle: 'italic' }}>
This is useful for critical confirmations or required actions.
</Text>
<View style={{ flexDirection: 'row', gap: 12, marginTop: 12 }}>
<Button variant='outline' onPress={close} style={{ flex: 1 }}>
Cancel
</Button>
<Button variant='destructive' onPress={close} style={{ flex: 1 }}>
Confirm
</Button>
</View>
</View>
</BottomSheet>
</View>
);
}
Custom Styling
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useThemeColor } from '@/hooks/useThemeColor';
import React from 'react';
export function BottomSheetStyled() {
const { isVisible, open, close } = useBottomSheet();
const accentColor = useThemeColor({}, 'blue');
return (
<View>
<Button onPress={open}>Styled Sheet</Button>
<BottomSheet
isVisible={isVisible}
onClose={close}
title='Custom Styling'
snapPoints={[0.5, 0.8]}
style={{
borderTopWidth: 3,
borderTopColor: accentColor,
}}
>
<View style={{ gap: 16 }}>
<View
style={{
backgroundColor: accentColor + '20',
padding: 16,
borderRadius: 12,
}}
>
<Text variant='title' style={{ color: accentColor }}>
Premium Feature
</Text>
<Text style={{ marginTop: 8 }}>
This bottom sheet has custom styling including a colored border
and accent-colored content areas.
</Text>
</View>
<Button variant='success' onPress={close}>
Get Started
</Button>
</View>
</BottomSheet>
</View>
);
}
Menu Options
import { BottomSheet, useBottomSheet } from '@/components/ui/bottom-sheet';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useThemeColor } from '@/hooks/useThemeColor';
import { TouchableOpacity } from 'react-native';
const menuItems = [
{ id: 'edit', title: 'Edit', icon: 'βοΈ' },
{ id: 'share', title: 'Share', icon: 'π€' },
{ id: 'copy', title: 'Copy Link', icon: 'π' },
{ id: 'bookmark', title: 'Bookmark', icon: 'π' },
{ id: 'delete', title: 'Delete', icon: 'ποΈ', destructive: true },
];
export function BottomSheetMenu() {
const { isVisible, open, close } = useBottomSheet();
const textColor = useThemeColor({}, 'text');
const destructiveColor = useThemeColor({}, 'destructive');
const handleMenuAction = (action: string) => {
console.log('Menu action:', action);
close();
};
return (
<View>
<Button onPress={open}>Show Menu</Button>
<BottomSheet isVisible={isVisible} onClose={close} snapPoints={[0.4]}>
<View>
{menuItems.map((item, index) => (
<TouchableOpacity
key={item.id}
style={{
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderBottomWidth: index < menuItems.length - 1 ? 1 : 0,
borderBottomColor: 'rgba(0,0,0,0.1)',
}}
onPress={() => handleMenuAction(item.id)}
>
<Text style={{ fontSize: 20, marginRight: 16 }}>{item.icon}</Text>
<Text
variant='body'
style={{
flex: 1,
color: item.destructive ? destructiveColor : textColor,
}}
>
{item.title}
</Text>
</TouchableOpacity>
))}
</View>
</BottomSheet>
</View>
);
}
API Reference
BottomSheet
The main bottom sheet component that provides a modal interface sliding from the bottom.
Prop | Type | Default | Description |
---|---|---|---|
isVisible | boolean | - | Controls the visibility of the bottom sheet. |
onClose | function | - | Callback function called when the sheet is closed. |
children | ReactNode | - | The content to display inside the bottom sheet. |
snapPoints | number[] | [0.3, 0.6, 0.9] | Array of snap points as percentages of screen height. |
enableBackdropDismiss | boolean | true | Whether tapping the backdrop should dismiss the sheet. |
title | string | - | Optional title to display at the top of the sheet. |
style | ViewStyle | - | Additional styles to apply to the bottom sheet container. |
useBottomSheet Hook
A custom hook that provides state management for the bottom sheet.
const { isVisible, open, close, toggle } = useBottomSheet();
Returns
Property | Type | Description |
---|---|---|
isVisible | boolean | Current visibility state of the sheet. |
open | function | Function to open the bottom sheet. |
close | function | Function to close the bottom sheet. |
toggle | function | Function to toggle the sheet visibility. |
Gesture Support
The BottomSheet component includes built-in gesture support:
- Pan Gesture: Drag the sheet up and down to resize
- Snap Points: The sheet will snap to predefined heights
- Velocity Detection: Fast downward swipes will close the sheet
- Boundary Limits: Prevents dragging beyond defined limits
Snap Points
Snap points define the available heights for the bottom sheet as percentages of screen height:
0.3
= 30% of screen height0.6
= 60% of screen height0.9
= 90% of screen height
The sheet will automatically snap to the nearest point when gestures end.
Animation
The component uses React Native Reanimated for smooth animations:
- Spring Animation: Natural feeling spring animations for opening/closing
- Gesture Responsiveness: Real-time tracking of pan gestures
- Backdrop Fade: Smooth opacity transitions for the backdrop
Accessibility
The BottomSheet component includes accessibility features:
- Modal Semantics: Proper modal behavior for screen readers
- Focus Management: Traps focus within the sheet when open
- Gesture Alternatives: Provides non-gesture ways to interact
- Backdrop Dismiss: Can be disabled for better accessibility control
Best Practices
- Content Height: Ensure your content works well with different snap points
- Backdrop Dismiss: Consider disabling for critical actions
- Loading States: Show loading indicators for async content
- Error Handling: Provide clear error states within the sheet
- Keyboard Handling: Account for keyboard appearance with form content