An extremely lightweight, opinionated toast component for React Native.
- Lightweight - only 20KB packed size
- New Architecture - built exclusively for React Native 0.76+
- Clean, imperative API inspired by Sonner
- Zero setup - add one component, start toasting. No hooks, no providers
- Smooth 60fps animations powered by Reanimated
- Natural swipe gestures that feel native to the platform
- Multiple toast types:
success,error,info,promise, andcustom - Promise handling with automatic loading → success/error states
- Toast stacking with configurable limits
- Works above modals - automatic on iOS, simple setup on Android
- RTL support - code-level RTL for when you're not using native RTL (
I18nManager) - Completely customizable - colors, icons, styles, animations
- Full Expo compatibility
- React Compiler compatible - all components are written to be optimized by react compiler
bun add react-native-breadThis package requires the following peer dependencies:
| Package | Version |
|---|---|
| react-native-reanimated | >= 4.1.0 |
| react-native-gesture-handler | >= 2.25.0 |
| react-native-safe-area-context | >= 5.0.0 |
| react-native-screens | >= 4.0.0 |
| react-native-svg | >= 15.8.0 |
| react-native-worklets | >= 0.5.0 |
If you don't have these installed, you can install all peer dependencies at once:
bun add react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-screens react-native-svg react-native-workletsOr with npm:
npm install react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-screens react-native-svg react-native-workletsNote: Make sure your
react-native-reanimatedandreact-native-workletsversions are compatible. Reanimated 4.1.x works with worklets 0.5.x-0.7.x, while Reanimated 4.2.x requires worklets 0.7.x only.
import { BreadLoaf } from 'react-native-bread';
function App() {
return (
<View>
<NavigationContainer>...</NavigationContainer>
<BreadLoaf />
</View>
);
}When using Expo Router, place the BreadLoaf component in your root layout file (app/_layout.tsx):
import { BreadLoaf } from 'react-native-bread';
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<BreadLoaf />
</>
);
}This ensures the toasts will be displayed across all screens in your app.
import { toast } from 'react-native-bread';
// Basic usage
toast.success('Saved!');
// With description
toast.success('Saved!', 'Your changes have been saved');
toast.error('Error', 'Something went wrong');
toast.info('Tip', 'Swipe to dismiss');
// Promise toast - shows loading, then success/error
toast.promise(fetchData(), {
loading: { title: 'Loading...', description: 'Please wait' },
success: { title: 'Done!', description: 'Data loaded' },
error: (err) => ({ title: 'Failed', description: err.message }),
});
// Custom toast - fully custom content with animations
toast.custom(({ dismiss }) => (
<View style={{ padding: 16, flexDirection: 'row', alignItems: 'center' }}>
<Image source={{ uri: 'avatar.png' }} style={{ width: 40, height: 40 }} />
<Text>New message from John</Text>
<Button title="Reply" onPress={dismiss} />
</View>
));Pass an options object as the second argument to customize individual toasts:
toast.success('Saved!', {
description: 'Your changes have been saved',
duration: 5000,
icon: <CustomIcon />,
style: { backgroundColor: '#fff' },
dismissible: true,
showCloseButton: true,
deduplication: false, // Opt out of deduplication for this toast
});Create fully custom toasts where you control all the content. Your component fills the entire toast container and receives all entry/exit/stack animations automatically:
// Using a render function (recommended - gives access to dismiss)
toast.custom(({ dismiss, id, type, isExiting }) => (
<View style={{ padding: 16, flexDirection: 'row', alignItems: 'center', gap: 12 }}>
<Image source={{ uri: 'avatar.png' }} style={{ width: 44, height: 44, borderRadius: 22 }} />
<View style={{ flex: 1 }}>
<Text style={{ fontWeight: '600' }}>New message</Text>
<Text style={{ color: '#666' }}>Hey, check this out!</Text>
</View>
<Pressable onPress={dismiss}>
<Text style={{ color: '#3b82f6' }}>Reply</Text>
</Pressable>
</View>
));
// Or pass a React component directly
toast.custom(<MyNotificationCard />);
// With options
toast.custom(<MyToast />, {
duration: 5000,
dismissible: false,
style: { backgroundColor: '#fef2f2' }
});Customize all toasts globally via the config prop on <BreadLoaf />:
<BreadLoaf
config={{
position: 'bottom',
rtl: false, // Code-level RTL — not needed if using native RTL (I18nManager)
stacking: true,
maxStack: 3,
defaultDuration: 4000,
colors: {
success: { accent: '#22c55e', background: '#f0fdf4' },
error: { accent: '#ef4444', background: '#fef2f2' },
}
}}
/>Available options include:
- position:
'top' | 'bottom'- Where toasts appear - offset: Extra spacing from screen edge
- stacking: Show multiple toasts stacked
- maxStack: Max visible toasts when stacking
- dismissible: Allow swipe to dismiss
- showCloseButton: Show X button
- defaultDuration: Default display time in ms
- deduplication: Prevent duplicate toasts (default:
true, see below) - colors: Custom colors per toast type
- icons: Custom icons per toast type
- toastStyle, titleStyle, descriptionStyle: Global style overrides
Deduplication is enabled by default. When the same toast is shown repeatedly (e.g., rapid button taps), it prevents stacking identical toasts. Instead, it resets the timer and plays a feedback animation:
- Non-error toasts: subtle pulse (scale bump)
- Error toasts: shake effect
Disable globally:
<BreadLoaf config={{ deduplication: false }} />Or per-toast (overrides global config):
// Opt out for a specific toast
toast.info('New message', { deduplication: false });
// Explicitly enable for a specific toast (redundant when global is on)
toast.success('Liked!', { deduplication: true });By default, a toast is considered a duplicate when it matches the front toast by title, type, and description. For stable matching across different content, provide an id — the existing toast's content will be updated:
toast.success('Saved item 1', { deduplication: true, id: 'save-action' });
toast.success('Saved item 2', { deduplication: true, id: 'save-action' }); // updates content, resets timer| Method | Description |
|---|---|
toast.success(title, description?) |
Show success toast |
toast.error(title, description?) |
Show error toast |
toast.info(title, description?) |
Show info toast |
toast.promise(promise, messages) |
Show loading → success/error toast |
toast.custom(content, options?) |
Show fully custom toast with your own content |
toast.dismiss(id) |
Dismiss a specific toast |
toast.dismissAll() |
Dismiss all toasts |
Toasts automatically appear above native modals on iOS.
On Android, you have two options:
The simplest fix is to use containedModal presentation instead of modal. On Android, modal and containedModal look nearly identical, so this is an easy swap:
<Stack.Screen
name="(modal)"
options={{ presentation: Platform.OS === "android" ? "containedModal" : "modal" }}
/>This renders the modal within the React hierarchy on Android, so toasts from your root <BreadLoaf /> remain visible.
If you need native modals, add <ToastPortal /> inside your modal layouts:
// app/(modal)/_layout.tsx
import { Stack } from "expo-router";
import { ToastPortal } from "react-native-bread";
export default function ModalLayout() {
return (
<>
<Stack screenOptions={{ headerShown: false }} />
<ToastPortal />
</>
);
}The ToastPortal component only renders on Android - it returns null on iOS, so no platform check is needed.
