Rainbow provides a comprehensive sheet system for presenting modal content, bottom sheets, and overlays.
Portal Sheet System
The Portal is Rainbow’s core sheet system for presenting modal content.
Basic Usage
import * as p from '@/screens/Portal';
import { Box, Stack, Text } from '@/design-system';
function MyComponent() {
const { open } = p.useOpen();
const handleOpenSheet = () => {
open(
() => (
<Box padding="20px">
<Stack space="12px">
<Text size="20pt" weight="bold" color="label">
Sheet Title
</Text>
<Text size="15pt" weight="regular" color="labelSecondary">
Sheet content goes here
</Text>
</Stack>
</Box>
),
{
sheetHeight: 400,
}
);
};
return (
<ButtonPressAnimation onPress={handleOpenSheet}>
<Text size="17pt" weight="semibold" color="blue">
Open Sheet
</Text>
</ButtonPressAnimation>
);
}
Portal API
useOpen Hook
Use within React components:
const { open } = p.useOpen();
open(
() => <YourSheetContent />,
{
sheetHeight: 500,
// ... other options
}
);
Imperative API
Use outside React components:
import * as p from '@/screens/Portal';
// Open sheet
p.open(
() => <YourSheetContent />,
{ sheetHeight: 400 }
);
// Close sheet
p.close();
Portal Options
Component to render in the sheet
Height of the sheet in pixels
Example: Confirmation Sheet
import * as p from '@/screens/Portal';
import { Box, Stack, Text } from '@/design-system';
import { SheetActionButtonRow } from '@/components/sheet/sheet-action-buttons/SheetActionButtonRow';
import SheetActionButton from '@/components/sheet/sheet-action-buttons/SheetActionButton';
function openConfirmationSheet(onConfirm: () => void) {
p.open(
() => (
<Box padding="20px">
<Stack space="20px">
<Stack space="8px">
<Text size="20pt" weight="bold" color="label" align="center">
Confirm Action
</Text>
<Text size="15pt" weight="regular" color="labelSecondary" align="center">
Are you sure you want to proceed?
</Text>
</Stack>
<SheetActionButtonRow>
<SheetActionButton
label="Cancel"
onPress={p.close}
color="#8E8E93"
/>
<SheetActionButton
label="Confirm"
onPress={() => {
onConfirm();
p.close();
}}
color="#007AFF"
size="big"
/>
</SheetActionButtonRow>
</Stack>
</Box>
),
{
sheetHeight: 280,
}
);
}
SlackSheet
A customizable bottom sheet with drag-to-dismiss behavior.
Props
Content to render in the sheet
Background color of the sheet
Border radius of the sheet
Fixed height of the sheet
Whether the sheet content is scrollable
Remove default top padding
Content height for calculations
Usage
import SlackSheet from '@/components/sheet/SlackSheet';
import { Stack, Text } from '@/design-system';
function MySheet() {
return (
<SlackSheet
backgroundColor="#FFFFFF"
borderRadius={30}
height={500}
>
<Stack space="16px" paddingHorizontal="20px">
<Text size="26pt" weight="bold" color="label">
Sheet Title
</Text>
<Text size="15pt" weight="regular" color="labelSecondary">
Sheet content...
</Text>
</Stack>
</SlackSheet>
);
}
SimpleSheet
A simplified sheet component for basic use cases.
Props
Whether the sheet can be dismissed by dragging
Usage
import { SimpleSheet } from '@/components/sheet/SimpleSheet';
import { Box, Text } from '@/design-system';
function BasicSheet() {
return (
<SimpleSheet
backgroundColor="#F5F5F5"
borderRadius={24}
dismissable
>
<Box padding="20px">
<Text size="17pt" weight="semibold" color="label">
Simple sheet content
</Text>
</Box>
</SimpleSheet>
);
}
Sheet Handle
Visual handle indicator for draggable sheets.
SheetHandle
import SheetHandle from '@/components/sheet/SheetHandle';
function MySheet() {
return (
<>
<SheetHandle color="#CCCCCC" showBlur={false} />
<Box padding="20px">
{/* Sheet content */}
</Box>
</>
);
}
SheetHandleFixedToTop
Handle that stays fixed to the top:
import SheetHandleFixedToTop from '@/components/sheet/SheetHandleFixedToTop';
function MySheet() {
return (
<>
<SheetHandleFixedToTop
color="#CCCCCC"
showBlur
top={12}
/>
<Box padding="20px">
{/* Sheet content */}
</Box>
</>
);
}
Sheet Utilities
SheetGestureBlocker
Blocks gestures from propagating through the sheet:
import { SheetGestureBlocker } from '@/components/sheet/SheetGestureBlocker';
<SheetGestureBlocker>
<ScrollableContent />
</SheetGestureBlocker>
SheetSubtitleCycler
Cycles through multiple subtitles:
import { SheetSubtitleCycler } from '@/components/sheet/SheetSubtitleCycler';
const items = [
{ subtitle: 'Loading...' },
{ subtitle: 'Almost there...' },
{ subtitle: 'Done!' },
];
<SheetSubtitleCycler
items={items}
interval={2000}
sharedValue={animatedValue}
/>
Common Patterns
Full-Screen Sheet
import * as p from '@/screens/Portal';
import { Dimensions } from 'react-native';
const { height } = Dimensions.get('window');
p.open(
() => <YourFullScreenContent />,
{
sheetHeight: height - 50, // Account for status bar
}
);
Sheet with Header and Actions
import * as p from '@/screens/Portal';
import { Box, Stack, Text, Rows, Row } from '@/design-system';
import { SheetActionButtonRow } from '@/components/sheet/sheet-action-buttons/SheetActionButtonRow';
import SheetActionButton from '@/components/sheet/sheet-action-buttons/SheetActionButton';
import SheetHandle from '@/components/sheet/SheetHandle';
function openActionSheet() {
p.open(
() => (
<Rows space="20px">
<Row height="content">
<Stack space="12px">
<SheetHandle />
<Box paddingHorizontal="20px">
<Text size="26pt" weight="bold" color="label">
Select an Option
</Text>
</Box>
</Stack>
</Row>
<Row>
<ScrollableContent />
</Row>
<Row height="content">
<SheetActionButtonRow>
<SheetActionButton
label="Cancel"
onPress={p.close}
color="#8E8E93"
/>
<SheetActionButton
label="Confirm"
onPress={() => {
handleConfirm();
p.close();
}}
color="#007AFF"
size="big"
/>
</SheetActionButtonRow>
</Row>
</Rows>
),
{
sheetHeight: 600,
}
);
}
Sheet with Loading State
import * as p from '@/screens/Portal';
import { useState } from 'react';
import { Box, Stack, Text } from '@/design-system';
import Spinner from '@/components/Spinner';
function LoadingSheet() {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
loadData().then(() => setIsLoading(false));
}, []);
if (isLoading) {
return (
<Box
padding="40px"
alignItems="center"
justifyContent="center"
>
<Spinner size={32} />
</Box>
);
}
return (
<Box padding="20px">
<Text size="17pt" weight="semibold" color="label">
Content loaded!
</Text>
</Box>
);
}
p.open(
() => <LoadingSheet />,
{ sheetHeight: 400 }
);
Best Practices
1. Use Appropriate Sheet Heights
Set sheet height based on content:
// ✅ Good - height matches content
p.open(
() => <CompactContent />,
{ sheetHeight: 300 }
);
// ❌ Avoid - height too large for content
p.open(
() => <CompactContent />,
{ sheetHeight: 800 }
);
2. Always Provide a Close Action
Make it easy to dismiss sheets:
<SheetActionButtonRow>
<SheetActionButton
label="Cancel"
onPress={p.close}
/>
<SheetActionButton
label="Confirm"
onPress={handleConfirm}
/>
</SheetActionButtonRow>
3. Handle Safe Area Insets
Account for device safe areas:
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function MySheet() {
const insets = useSafeAreaInsets();
return (
<Box paddingBottom={{ custom: insets.bottom + 20 }}>
{/* Content */}
</Box>
);
}
4. Use Semantic Colors
Use design system colors for consistency:
// ✅ Good
<SlackSheet backgroundColor="surfacePrimary">
// ❌ Avoid
<SlackSheet backgroundColor="#FFFFFF">