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 { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
export function SheetDemo() {
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger>
<Button>Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Welcome to the Sheet</SheetTitle>
<SheetDescription>
This is a basic sheet component that slides in from the right side
of the screen.
</SheetDescription>
</SheetHeader>
<View style={{ padding: 24, gap: 16 }}>
<Text>
This sheet can contain any content you need. It's perfect for
navigation menus, forms, settings, or detailed information.
</Text>
<Button onPress={() => setOpen(false)}>Close Sheet</Button>
</View>
</SheetContent>
</Sheet>
);
}
Installation
pnpm dlx bna-ui add sheet
Usage
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
<Sheet>
<SheetTrigger>
<Button>Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Sheet Title</SheetTitle>
<SheetDescription>
This is a description of the sheet content.
</SheetDescription>
</SheetHeader>
{/* Your content here */}
</SheetContent>
</Sheet>
Examples
Default
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
export function SheetDemo() {
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger>
<Button>Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Welcome to the Sheet</SheetTitle>
<SheetDescription>
This is a basic sheet component that slides in from the right side
of the screen.
</SheetDescription>
</SheetHeader>
<View style={{ padding: 24, gap: 16 }}>
<Text>
This sheet can contain any content you need. It's perfect for
navigation menus, forms, settings, or detailed information.
</Text>
<Button onPress={() => setOpen(false)}>Close Sheet</Button>
</View>
</SheetContent>
</Sheet>
);
}
Left Side
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';
export function SheetLeft() {
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen} side='left'>
<SheetTrigger>
<Button>Open Left Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Left Side Sheet</SheetTitle>
<SheetDescription>
This sheet slides in from the left side of the screen.
</SheetDescription>
</SheetHeader>
<View style={{ padding: 24, gap: 16 }}>
<Text>
Left-side sheets are commonly used for navigation menus and primary
actions that need to be easily accessible.
</Text>
<Button onPress={() => setOpen(false)}>Close Sheet</Button>
</View>
</SheetContent>
</Sheet>
);
}
Navigation Menu
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/ui/icon';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useThemeColor } from '@/hooks/useThemeColor';
import { Bell, Home, Mail, Search, Settings, User } from 'lucide-react-native';
import React, { useState } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
export function SheetNavigation() {
const [open, setOpen] = useState(false);
const [activeItem, setActiveItem] = useState('home');
const textColor = useThemeColor({}, 'text');
const mutedColor = useThemeColor({}, 'textMuted');
const borderColor = useThemeColor({}, 'border');
const navigationItems = [
{ id: 'home', label: 'Home', icon: Home },
{ id: 'profile', label: 'Profile', icon: User },
{ id: 'messages', label: 'Messages', icon: Mail },
{ id: 'search', label: 'Search', icon: Search },
{ id: 'notifications', label: 'Notifications', icon: Bell },
{ id: 'settings', label: 'Settings', icon: Settings },
];
const handleItemPress = (itemId: string) => {
setActiveItem(itemId);
setOpen(false);
};
return (
<Sheet open={open} onOpenChange={setOpen} side='left'>
<SheetTrigger>
<Button>Open Navigation</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Navigation Menu</SheetTitle>
<SheetDescription>
Navigate to different sections of the app.
</SheetDescription>
</SheetHeader>
<View style={styles.navigationContainer}>
{navigationItems.map((item) => {
const name = item.icon;
const isActive = activeItem === item.id;
return (
<TouchableOpacity
key={item.id}
style={[
styles.navigationItem,
{
backgroundColor: isActive
? `${textColor}10`
: 'transparent',
borderColor,
},
]}
onPress={() => handleItemPress(item.id)}
>
<Icon
name={name}
size={20}
color={isActive ? textColor : mutedColor}
/>
<Text
style={[
styles.navigationText,
{ color: isActive ? textColor : mutedColor },
]}
>
{item.label}
</Text>
</TouchableOpacity>
);
})}
</View>
</SheetContent>
</Sheet>
);
}
const styles = StyleSheet.create({
navigationContainer: {
padding: 16,
gap: 8,
},
navigationItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
padding: 12,
borderRadius: 8,
borderWidth: 1,
borderColor: 'transparent',
},
navigationText: {
fontSize: 16,
fontWeight: '500',
},
});
Form Sheet
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useThemeColor } from '@/hooks/useThemeColor';
import React, { useState } from 'react';
import { Alert, StyleSheet, TextInput } from 'react-native';
export function SheetForm() {
const [open, setOpen] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const textColor = useThemeColor({}, 'text');
const backgroundColor = useThemeColor({}, 'background');
const borderColor = useThemeColor({}, 'border');
const mutedColor = useThemeColor({}, 'textMuted');
const handleSubmit = () => {
if (!formData.name || !formData.email || !formData.message) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
Alert.alert('Success', 'Form submitted successfully!');
setFormData({ name: '', email: '', message: '' });
setOpen(false);
};
const handleReset = () => {
setFormData({ name: '', email: '', message: '' });
};
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger>
<Button>Open Contact Form</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Contact Us</SheetTitle>
<SheetDescription>
Fill out the form below and we'll get back to you soon.
</SheetDescription>
</SheetHeader>
<View style={styles.formContainer}>
<View style={styles.fieldContainer}>
<Text style={[styles.label, { color: textColor }]}>Name</Text>
<TextInput
style={[
styles.input,
{
borderColor,
backgroundColor,
color: textColor,
},
]}
value={formData.name}
onChangeText={(text) =>
setFormData((prev) => ({ ...prev, name: text }))
}
placeholder='Enter your name'
placeholderTextColor={mutedColor}
/>
</View>
<View style={styles.fieldContainer}>
<Text style={[styles.label, { color: textColor }]}>Email</Text>
<TextInput
style={[
styles.input,
{
borderColor,
backgroundColor,
color: textColor,
},
]}
value={formData.email}
onChangeText={(text) =>
setFormData((prev) => ({ ...prev, email: text }))
}
placeholder='Enter your email'
placeholderTextColor={mutedColor}
keyboardType='email-address'
autoCapitalize='none'
/>
</View>
<View style={styles.fieldContainer}>
<Text style={[styles.label, { color: textColor }]}>Message</Text>
<TextInput
style={[
styles.input,
styles.textArea,
{
borderColor,
backgroundColor,
color: textColor,
},
]}
value={formData.message}
onChangeText={(text) =>
setFormData((prev) => ({ ...prev, message: text }))
}
placeholder='Enter your message'
placeholderTextColor={mutedColor}
multiline
numberOfLines={4}
textAlignVertical='top'
/>
</View>
<View style={styles.buttonContainer}>
<Button style={styles.button} onPress={handleSubmit}>
Submit
</Button>
<Button
variant='outline'
style={styles.button}
onPress={handleReset}
>
Reset
</Button>
</View>
</View>
</SheetContent>
</Sheet>
);
}
const styles = StyleSheet.create({
formContainer: {
padding: 24,
gap: 20,
},
fieldContainer: {
gap: 8,
},
label: {
fontSize: 16,
fontWeight: '500',
},
input: {
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 16,
},
textArea: {
height: 100,
},
buttonContainer: {
flexDirection: 'row',
gap: 12,
marginTop: 12,
},
button: {
flex: 1,
},
});
Filter Sheet
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useThemeColor } from '@/hooks/useThemeColor';
import { Filter } from 'lucide-react-native';
import React, { useState } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
export function SheetFilter() {
const [open, setOpen] = useState(false);
const [filters, setFilters] = useState({
category: 'all',
price: 'all',
rating: 'all',
brand: 'all',
});
const textColor = useThemeColor({}, 'text');
const mutedColor = useThemeColor({}, 'textMuted');
const borderColor = useThemeColor({}, 'border');
const filterOptions = {
category: [
{ value: 'all', label: 'All Categories' },
{ value: 'electronics', label: 'Electronics' },
{ value: 'clothing', label: 'Clothing' },
{ value: 'books', label: 'Books' },
{ value: 'home', label: 'Home & Garden' },
],
price: [
{ value: 'all', label: 'Any Price' },
{ value: 'under-25', label: 'Under $25' },
{ value: '25-50', label: '$25 - $50' },
{ value: '50-100', label: '$50 - $100' },
{ value: 'over-100', label: 'Over $100' },
],
rating: [
{ value: 'all', label: 'Any Rating' },
{ value: '4-plus', label: '4+ Stars' },
{ value: '3-plus', label: '3+ Stars' },
{ value: '2-plus', label: '2+ Stars' },
],
brand: [
{ value: 'all', label: 'All Brands' },
{ value: 'apple', label: 'Apple' },
{ value: 'samsung', label: 'Samsung' },
{ value: 'nike', label: 'Nike' },
{ value: 'adidas', label: 'Adidas' },
],
};
const handleFilterChange = (
filterType: keyof typeof filters,
value: string
) => {
setFilters((prev) => ({ ...prev, [filterType]: value }));
};
const handleApplyFilters = () => {
// Apply filters logic here
console.log('Applied filters:', filters);
setOpen(false);
};
const handleClearFilters = () => {
setFilters({
category: 'all',
price: 'all',
rating: 'all',
brand: 'all',
});
};
const renderFilterSection = (
title: string,
filterType: keyof typeof filters,
options: { value: string; label: string }[]
) => (
<View style={styles.filterSection}>
<Text style={[styles.sectionTitle, { color: textColor }]}>{title}</Text>
<View style={styles.optionsContainer}>
{options.map((option) => (
<TouchableOpacity
key={option.value}
style={[
styles.option,
{
borderColor,
backgroundColor:
filters[filterType] === option.value
? `${textColor}10`
: 'transparent',
},
]}
onPress={() => handleFilterChange(filterType, option.value)}
>
<Text
style={[
styles.optionText,
{
color:
filters[filterType] === option.value
? textColor
: mutedColor,
},
]}
>
{option.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger>
<Button icon={Filter}>Filter</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Filter Products</SheetTitle>
<SheetDescription>
Refine your search results using the filters below.
</SheetDescription>
</SheetHeader>
<View style={styles.filterContainer}>
{renderFilterSection('Category', 'category', filterOptions.category)}
{renderFilterSection('Price Range', 'price', filterOptions.price)}
{renderFilterSection('Rating', 'rating', filterOptions.rating)}
{renderFilterSection('Brand', 'brand', filterOptions.brand)}
<View style={styles.buttonContainer}>
<Button style={styles.button} onPress={handleApplyFilters}>
Apply Filters
</Button>
<Button
variant='outline'
style={styles.button}
onPress={handleClearFilters}
>
Clear All
</Button>
</View>
</View>
</SheetContent>
</Sheet>
);
}
const styles = StyleSheet.create({
filterContainer: {
padding: 16,
gap: 24,
},
filterSection: {
gap: 12,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
},
optionsContainer: {
gap: 8,
},
option: {
padding: 12,
borderRadius: 8,
borderWidth: 1,
},
optionText: {
fontSize: 16,
},
buttonContainer: {
flexDirection: 'row',
gap: 12,
marginTop: 12,
},
button: {
flex: 1,
},
});
API Reference
Sheet
The root component that manages the sheet state and provides context to child components.
Prop | Type | Default | Description |
---|---|---|---|
open | boolean | - | Controls whether the sheet is open or closed. |
onOpenChange | (open: boolean) => void | - | Callback fired when the sheet state changes. |
side | 'left' | 'right' | 'right' | The side from which the sheet slides in. |
children | ReactNode | - | The sheet trigger and content components. |
SheetTrigger
The trigger component that opens the sheet when pressed.
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The trigger content (usually a button or text). |
asChild | boolean | false | Whether to render as a child component. |
SheetContent
The main content container that slides in from the specified side.
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The content to display inside the sheet. |
style | ViewStyle | - | Additional styles to apply to the content container. |
SheetHeader
A header component that provides consistent spacing and layout for the sheet title and description.
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The header content (title and description). |
style | ViewStyle | - | Additional styles to apply to the header. |
SheetTitle
The title component for the sheet header.
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The title text or content. |
SheetDescription
The description component for the sheet header.
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The description text or content. |
Accessibility
The Sheet component is built with accessibility in mind:
- Uses native Modal component for proper focus management
- Includes proper ARIA attributes for screen readers
- Supports keyboard navigation and dismissal
- Close button is positioned for easy access
- Overlay can be pressed to dismiss the sheet
- Proper hit areas for touch targets
Animation
The Sheet component includes smooth animations:
- Slides in from the specified side (left or right)
- Fades in the overlay backdrop
- Animates out when dismissed
- Uses native animations for optimal performance
Notes
- The sheet automatically handles safe area insets
- Maximum width is set to 80% of screen width or 400px, whichever is smaller
- Includes platform-specific shadow styling for iOS and Android
- Close button position adapts based on the sheet side