react-native-toast-message-library 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/LICENSE +21 -0
- package/README.md +762 -2
- package/package.json +82 -4
- package/src/animations/toastAnimations.js +85 -0
- package/src/components/ToastAction.js +39 -0
- package/src/components/ToastContainer.js +109 -0
- package/src/components/ToastIcon.js +65 -0
- package/src/components/ToastItem.js +357 -0
- package/src/components/ToastProgress.js +69 -0
- package/src/components/ToastProvider.js +107 -0
- package/src/context/ToastContext.js +5 -0
- package/src/global.js +38 -0
- package/src/hooks/useReducedMotion.js +28 -0
- package/src/hooks/useToast.js +13 -0
- package/src/index.js +4 -0
- package/src/manager/ToastManager.js +399 -0
- package/src/manager/createToastApi.js +33 -0
- package/src/themes/dark.js +64 -0
- package/src/themes/index.js +73 -0
- package/src/themes/light.js +64 -0
- package/src/utils/constants.js +40 -0
- package/src/utils/createAnnouncer.js +46 -0
- package/src/utils/generateToastId.js +10 -0
- package/src/utils/normalizeToastOptions.js +42 -0
- package/src/utils/safeArea.js +34 -0
- package/src/utils/validateToastOptions.js +127 -0
- package/types/index.d.ts +203 -0
package/README.md
CHANGED
|
@@ -1,3 +1,763 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-native-toast-message-library
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lightweight, customizable toast notifications for React Native.
|
|
4
|
+
|
|
5
|
+
- Pure JavaScript: no native modules, no linking, no required dependencies.
|
|
6
|
+
- Built on the `Animated` API, with a FIFO queue, themes, action buttons, swipe-to-dismiss and accessibility support.
|
|
7
|
+
- TypeScript declarations included.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Table of contents
|
|
12
|
+
|
|
13
|
+
1. [Features](#features)
|
|
14
|
+
2. [Compatibility](#compatibility)
|
|
15
|
+
3. [Installation](#installation)
|
|
16
|
+
4. [Quick start](#quick-start)
|
|
17
|
+
5. [Showing toasts](#showing-toasts)
|
|
18
|
+
6. [Title and description](#title-and-description)
|
|
19
|
+
7. [Positions: top, bottom and more](#positions-top-bottom-and-more)
|
|
20
|
+
8. [Safe-area insets and offsets](#safe-area-insets-and-offsets)
|
|
21
|
+
9. [Options reference (per toast)](#options-reference-per-toast)
|
|
22
|
+
10. [Provider props reference](#provider-props-reference)
|
|
23
|
+
11. [Callbacks](#callbacks)
|
|
24
|
+
12. [Queue behaviour](#queue-behaviour)
|
|
25
|
+
13. [Themes](#themes)
|
|
26
|
+
14. [Custom icons and content](#custom-icons-and-content)
|
|
27
|
+
15. [Action buttons](#action-buttons)
|
|
28
|
+
16. [Animations and reduced motion](#animations-and-reduced-motion)
|
|
29
|
+
17. [Accessibility](#accessibility)
|
|
30
|
+
18. [TypeScript](#typescript)
|
|
31
|
+
19. [Recipes (copy and paste examples)](#recipes-copy-and-paste-examples)
|
|
32
|
+
20. [Troubleshooting](#troubleshooting)
|
|
33
|
+
21. [Limitations](#limitations)
|
|
34
|
+
22. [Development](#development)
|
|
35
|
+
23. [Publishing](#publishing)
|
|
36
|
+
24. [Contributing](#contributing) · [License](#license)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- Types: `success`, `error`, `warning`, `info`, `custom`
|
|
43
|
+
- Seven positions: `top`, `bottom`, `center`, `top-left`, `top-right`, `bottom-left`, `bottom-right` (chosen per toast)
|
|
44
|
+
- Custom **title** and **description**, or remove either one (per toast or for every toast of a type)
|
|
45
|
+
- Safe-area aware, with extra `insets`, provider `offset` and per-toast `offsetX` / `offsetY`
|
|
46
|
+
- Animations: `fade`, `slide`, `zoom`, `none` (native driver, reduced-motion aware)
|
|
47
|
+
- Auto-dismiss, persistent toasts (`duration: 0`); the timer pauses while the user touches or swipes a toast
|
|
48
|
+
- FIFO queue with `maxVisible`; dismiss by id, `hideAll()`, `isVisible()`
|
|
49
|
+
- Close button, leading icon, action button, progress bar, swipe-to-dismiss, tap handler
|
|
50
|
+
- Update a toast in place by re-using its `id` (for example "Uploading…" → "Done")
|
|
51
|
+
- `onShow`, `onPress`, `onAction`, `onDismiss(reason)` callbacks
|
|
52
|
+
- Light / dark / system themes, `createTheme()`, per-toast style overrides, custom icons and fully custom content
|
|
53
|
+
- Accessibility roles and labels, Android live region, throttled iOS announcements
|
|
54
|
+
- Optional vibration feedback (`haptics`)
|
|
55
|
+
|
|
56
|
+
## Compatibility
|
|
57
|
+
|
|
58
|
+
| What | Status |
|
|
59
|
+
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
60
|
+
| React Native 0.88.0 / React 19.3.0 | **Verified**: unit tests (Jest) and Metro bundling for Android and iOS |
|
|
61
|
+
| Real devices / emulators | **Not verified in this repository's CI environment.** Run the demo app before relying on visuals. |
|
|
62
|
+
| Other RN / React versions | **Not verified.** Peer ranges (`react >=18`, `react-native >=0.72`) are lower bounds based on the APIs used (`useSyncExternalStore` needs React 18), not test results. |
|
|
63
|
+
| New / Old Architecture | Not separately tested. No native code is involved. |
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
npm install react-native-toast-message-library
|
|
69
|
+
|
|
70
|
+
# optional but recommended: exact safe-area insets (notch, status bar, home indicator)
|
|
71
|
+
npm install react-native-safe-area-context
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
No pods, no linking, no Gradle changes.
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
**Step 1 – wrap your app once, near the root.** If you use `react-native-safe-area-context`, put `SafeAreaProvider` outside `ToastProvider`.
|
|
79
|
+
|
|
80
|
+
```jsx
|
|
81
|
+
// App.js
|
|
82
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
83
|
+
import { ToastProvider } from 'react-native-toast-message-library';
|
|
84
|
+
|
|
85
|
+
export default function App() {
|
|
86
|
+
return (
|
|
87
|
+
<SafeAreaProvider>
|
|
88
|
+
<ToastProvider
|
|
89
|
+
position="top"
|
|
90
|
+
duration={3000}
|
|
91
|
+
animation="slide"
|
|
92
|
+
maxVisible={3}
|
|
93
|
+
>
|
|
94
|
+
<Home />
|
|
95
|
+
</ToastProvider>
|
|
96
|
+
</SafeAreaProvider>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Step 2 – show a toast from any component below the provider.**
|
|
102
|
+
|
|
103
|
+
```jsx
|
|
104
|
+
import { Button } from 'react-native';
|
|
105
|
+
import { useToast } from 'react-native-toast-message-library';
|
|
106
|
+
|
|
107
|
+
function Home() {
|
|
108
|
+
const toast = useToast();
|
|
109
|
+
return (
|
|
110
|
+
<Button
|
|
111
|
+
title="Save"
|
|
112
|
+
onPress={() =>
|
|
113
|
+
toast.success({
|
|
114
|
+
title: 'Saved',
|
|
115
|
+
description: 'Your changes have been saved.',
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The provider renders its children followed by the toast overlay inside a wrapper `View` with `flex: 1` (change it with the `style` prop).
|
|
124
|
+
|
|
125
|
+
## Showing toasts
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
const toast = useToast();
|
|
129
|
+
|
|
130
|
+
toast.show(options | 'message'); // -> id
|
|
131
|
+
toast.success(message, options); // -> id (also: error / warning / info)
|
|
132
|
+
toast.hide(id); // -> boolean (true if a visible or queued toast was dismissed)
|
|
133
|
+
toast.hideAll(); // -> number of toasts dismissed
|
|
134
|
+
toast.isVisible(id?); // -> boolean (without an id: is any toast visible?)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- `success`, `error`, `warning` and `info` accept a string, a number **or** an options object as the first argument. The type is always forced to the method's type.
|
|
138
|
+
- Invalid options **throw a `TypeError`** that explains the problem (unknown `position`, negative `duration`, non-function callbacks, nothing to show, …). Nothing is silently ignored.
|
|
139
|
+
- `useToast()` outside a `ToastProvider` throws an error telling you to add one.
|
|
140
|
+
- The returned object is stable: components that call `useToast()` do not re-render when toasts change.
|
|
141
|
+
|
|
142
|
+
### Global API (opt-in, for code outside React)
|
|
143
|
+
|
|
144
|
+
```jsx
|
|
145
|
+
import { ToastProvider, Toast } from 'react-native-toast-message-library';
|
|
146
|
+
|
|
147
|
+
<ToastProvider global>…</ToastProvider>;
|
|
148
|
+
|
|
149
|
+
// anywhere: services, API clients, interceptors
|
|
150
|
+
Toast.error('Network request failed');
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`global` registers that provider when it mounts and unregisters it on unmount. If no global provider is mounted, `Toast.*` **throws** (it never fails silently). With several global providers the most recently mounted one wins. Prefer the hook inside components.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Title and description
|
|
158
|
+
|
|
159
|
+
Every toast has two optional text parts:
|
|
160
|
+
|
|
161
|
+
| Part | Option | Default |
|
|
162
|
+
| --------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
163
|
+
| **Title** (bold heading) | `title` | The type name: `Success`, `Error`, `Warning`, `Info` (no title for `custom`) |
|
|
164
|
+
| **Description** (body text) | `message` or `description` (identical; use one of them) | none |
|
|
165
|
+
|
|
166
|
+
### Change them
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
// 1) Custom title + custom description
|
|
170
|
+
toast.success({
|
|
171
|
+
title: 'Profile saved',
|
|
172
|
+
description: 'Everyone can see your new photo.',
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// 2) Shortcut form: first argument is the description, the title comes from the options
|
|
176
|
+
toast.error('Card declined.', { title: 'Payment failed' });
|
|
177
|
+
|
|
178
|
+
// 3) `message` and `description` are the same thing
|
|
179
|
+
toast.info({ title: 'Update', message: 'Version 2.0 is available.' });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Remove them
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
// Remove the title: pass '' or null. Only the description is shown.
|
|
186
|
+
toast.error('Could not reach the server.', { title: '' });
|
|
187
|
+
|
|
188
|
+
// Remove the description: just leave it out. Only the title is shown.
|
|
189
|
+
toast.info({ title: 'Sync finished' });
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
A toast must show something. A toast with an empty title **and** an empty description (and no `render`) throws a `TypeError`.
|
|
193
|
+
|
|
194
|
+
### Change the default title of every toast of a type
|
|
195
|
+
|
|
196
|
+
Use the `titles` prop on the provider. This is also the way to translate the default titles.
|
|
197
|
+
|
|
198
|
+
```jsx
|
|
199
|
+
<ToastProvider
|
|
200
|
+
titles={{
|
|
201
|
+
success: 'Done',
|
|
202
|
+
error: 'Something went wrong',
|
|
203
|
+
warning: 'Heads up',
|
|
204
|
+
info: 'FYI',
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Hide the default title for all toasts (a per-toast `title` still works):
|
|
210
|
+
|
|
211
|
+
```jsx
|
|
212
|
+
<ToastProvider titles={{ success: '', error: '', warning: '', info: '' }} />
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Order of priority for the title: **per-toast `title`** → **provider `titles[type]`** → **built-in type name**.
|
|
216
|
+
|
|
217
|
+
### Style the title and description
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
toast.success({
|
|
221
|
+
title: 'Saved',
|
|
222
|
+
description: 'All changes are synced.',
|
|
223
|
+
titleStyle: { fontSize: 18, letterSpacing: 0.5 },
|
|
224
|
+
messageStyle: { fontStyle: 'italic' },
|
|
225
|
+
textColor: '#0B3D2E', // colour of both
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Global sizes, weights and fonts are set in the theme: `typography: { titleSize, titleWeight, messageSize, messageWeight, fontFamily }` (see [Themes](#themes)).
|
|
230
|
+
|
|
231
|
+
### Change the text of a toast that is already visible
|
|
232
|
+
|
|
233
|
+
Re-use the `id`. The toast is updated in place and its timer (and progress bar) restarts.
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
const id = toast.show({
|
|
237
|
+
id: 'upload',
|
|
238
|
+
type: 'info',
|
|
239
|
+
title: 'Uploading',
|
|
240
|
+
description: '0%',
|
|
241
|
+
duration: 0,
|
|
242
|
+
});
|
|
243
|
+
// later
|
|
244
|
+
toast.show({
|
|
245
|
+
id: 'upload',
|
|
246
|
+
type: 'success',
|
|
247
|
+
title: 'Uploaded',
|
|
248
|
+
description: 'Your file is ready.',
|
|
249
|
+
duration: 3000,
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
> Screen readers: the accessibility label is built as `"<type>. <title>. <description>"`. If you hide the title, the type is still announced. Set `accessibilityLabel` to override it completely.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Positions: top, bottom and more
|
|
258
|
+
|
|
259
|
+
`position` is chosen per toast (default `'top'`, or the provider's `position`):
|
|
260
|
+
|
|
261
|
+
| Position | Where it appears |
|
|
262
|
+
| ----------------------------- | --------------------------------- |
|
|
263
|
+
| `top` | Top edge, centred |
|
|
264
|
+
| `bottom` | Bottom edge, centred |
|
|
265
|
+
| `center` | Middle of the screen |
|
|
266
|
+
| `top-left`, `top-right` | Top edge, aligned left / right |
|
|
267
|
+
| `bottom-left`, `bottom-right` | Bottom edge, aligned left / right |
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
toast.success('Saved', { position: 'top' });
|
|
271
|
+
toast.error('Failed', { position: 'bottom' });
|
|
272
|
+
toast.info('Hello', { position: 'bottom-right' });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Set a default for all toasts on the provider and override it when needed:
|
|
276
|
+
|
|
277
|
+
```jsx
|
|
278
|
+
<ToastProvider position="bottom">…</ToastProvider>;
|
|
279
|
+
toast.info('This one is on top', { position: 'top' });
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
How toasts stack:
|
|
283
|
+
|
|
284
|
+
- Toasts that share a position stack in the order they were shown (oldest first, newest last). The gap between them is `spacing` (provider prop, default `10`).
|
|
285
|
+
- At most `maxVisible` toasts are on screen in total; the rest wait in the [queue](#queue-behaviour).
|
|
286
|
+
- Toasts are at most **480 dp** wide and always keep a **16 dp** margin from the screen sides.
|
|
287
|
+
- Each position is its own stack, so a `top` toast and a `bottom` toast can be visible at the same time.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Safe-area insets and offsets
|
|
292
|
+
|
|
293
|
+
Three things control the distance between a toast and the screen edges. They add up.
|
|
294
|
+
|
|
295
|
+
| Setting | Where | What it does |
|
|
296
|
+
| ------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
297
|
+
| **`insets`** | `<ToastProvider insets={{ top, bottom, left, right }}>` | The "unsafe" areas of the screen: status bar / notch at the top, home indicator at the bottom. |
|
|
298
|
+
| **`offset`** | `<ToastProvider offset={{ x, y }}>` | Extra distance for **every** toast: `y` away from the top or bottom edge, `x` away from the left and right sides. |
|
|
299
|
+
| **`offsetX` / `offsetY`** | per toast option | Extra distance for **one** toast, added on top of the provider `offset`. |
|
|
300
|
+
|
|
301
|
+
Resulting distance from the edge:
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
top toasts: insets.top + offset.y + toast.offsetY
|
|
305
|
+
bottom toasts: insets.bottom + offset.y + toast.offsetY
|
|
306
|
+
left side: 16 + insets.left + offset.x (+ toast.offsetX for *-left positions)
|
|
307
|
+
right side: 16 + insets.right + offset.x (+ toast.offsetX for *-right positions)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Notes: `center` toasts ignore `offsetY` and `offset.y`. For centred positions (`top`, `bottom`, `center`) a per-toast `offsetX` makes the toast narrower on both sides. All offsets must be numbers `>= 0`.
|
|
311
|
+
|
|
312
|
+
### Where do the insets come from?
|
|
313
|
+
|
|
314
|
+
In this order:
|
|
315
|
+
|
|
316
|
+
1. The `insets` prop of `ToastProvider` (any side you pass wins; sides you leave out are taken from the next source).
|
|
317
|
+
2. `react-native-safe-area-context`, if it is installed **and** your app is wrapped in `SafeAreaProvider`. This is exact and recommended.
|
|
318
|
+
3. A built-in fallback: Android `top` = status-bar height, `bottom` = 0; iOS `top` = 47, `bottom` = 34 (conservative notch values).
|
|
319
|
+
|
|
320
|
+
### Examples
|
|
321
|
+
|
|
322
|
+
**Recommended: let safe-area-context do it.** Nothing else is needed.
|
|
323
|
+
|
|
324
|
+
```jsx
|
|
325
|
+
<SafeAreaProvider>
|
|
326
|
+
<ToastProvider>
|
|
327
|
+
<App />
|
|
328
|
+
</ToastProvider>
|
|
329
|
+
</SafeAreaProvider>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Set the insets yourself** (no safe-area library):
|
|
333
|
+
|
|
334
|
+
```jsx
|
|
335
|
+
<ToastProvider insets={{ top: 44, bottom: 34 }}>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Only one side is fine; the other sides use the automatic value:
|
|
339
|
+
|
|
340
|
+
```jsx
|
|
341
|
+
<ToastProvider insets={{ bottom: 24 }}>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Keep toasts below your own header** (56 dp tall):
|
|
345
|
+
|
|
346
|
+
```jsx
|
|
347
|
+
<ToastProvider offset={{ y: 56 }} position="top">
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Keep bottom toasts above a bottom tab bar** (React Navigation example):
|
|
351
|
+
|
|
352
|
+
```jsx
|
|
353
|
+
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
|
354
|
+
|
|
355
|
+
function Screen() {
|
|
356
|
+
const toast = useToast();
|
|
357
|
+
const tabBarHeight = useBottomTabBarHeight();
|
|
358
|
+
const saved = () =>
|
|
359
|
+
toast.success('Saved', { position: 'bottom', offsetY: tabBarHeight });
|
|
360
|
+
// ...
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
…or for the whole app, if the tab bar is always visible:
|
|
365
|
+
|
|
366
|
+
```jsx
|
|
367
|
+
<ToastProvider position="bottom" offset={{ y: 64 }}>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Move one toast** away from the edge or side:
|
|
371
|
+
|
|
372
|
+
```js
|
|
373
|
+
toast.info('A bit lower', { position: 'top', offsetY: 24 });
|
|
374
|
+
toast.info('Narrower', { position: 'top-right', offsetX: 20 });
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**More space at the sides for every toast:**
|
|
378
|
+
|
|
379
|
+
```jsx
|
|
380
|
+
<ToastProvider offset={{ x: 8 }}> // 16 + 8 = 24 dp from each side
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
> The keyboard is not handled automatically. A `bottom` toast can be covered by the keyboard; use `offsetY` (for example the keyboard height from `Keyboard.addListener`) or show the toast at `top`.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Options reference (per toast)
|
|
388
|
+
|
|
389
|
+
| Option | Type | Default | Description |
|
|
390
|
+
| ---------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
391
|
+
| `id` | string \| number | generated | Re-using the id of a visible or queued toast **updates it in place** and restarts its timer |
|
|
392
|
+
| `type` | `'success' \| 'error' \| 'warning' \| 'info' \| 'custom'` | `'custom'` | Controls colours, icon and default title |
|
|
393
|
+
| `title` | string \| number \| null | type name (or provider `titles[type]`) | `''` or `null` hides the title |
|
|
394
|
+
| `message` | string \| number | – | The description. At least a title, a message or `render` is needed |
|
|
395
|
+
| `description` | string \| number | – | Alias of `message`. Do not pass both |
|
|
396
|
+
| `duration` | number (ms) | `3000` | `0` = persistent |
|
|
397
|
+
| `position` | see [Positions](#positions-top-bottom-and-more) | `'top'` | |
|
|
398
|
+
| `animation` | `'fade' \| 'slide' \| 'zoom' \| 'none'` | `'slide'` | |
|
|
399
|
+
| `animationDuration` | number (ms) | `250` | |
|
|
400
|
+
| `offsetX`, `offsetY` | number (>= 0) | – | Extra distance on top of the provider `offset` ([details](#safe-area-insets-and-offsets)) |
|
|
401
|
+
| `icon` | `false` \| element \| `({type,color,size}) => node` | built-in glyph | |
|
|
402
|
+
| `render` | `({id,type,hide,colors,theme}) => node` | – | Replaces icon + title + message |
|
|
403
|
+
| `closeButton` | boolean | `false` | Accessible "Dismiss notification" button |
|
|
404
|
+
| `swipeToDismiss` | boolean | `true` | Horizontal swipe |
|
|
405
|
+
| `progress` | boolean | `false` | Remaining-time bar (only when `duration > 0`) |
|
|
406
|
+
| `haptics` | boolean | `false` | Vibration, see [Limitations](#limitations) |
|
|
407
|
+
| `action` | `{ label, accessibilityLabel? }` | – | Action button |
|
|
408
|
+
| `dismissOnAction` | boolean | `true` | |
|
|
409
|
+
| `dismissOnPress` | boolean | `false` | |
|
|
410
|
+
| `backgroundColor`, `textColor`, `borderColor`, `iconColor` | string | theme | |
|
|
411
|
+
| `borderRadius`, `padding` | number | theme | |
|
|
412
|
+
| `shadow` | `false` \| `{color,opacity,radius,offsetY,elevation}` | theme | |
|
|
413
|
+
| `containerStyle`, `titleStyle`, `messageStyle` | style | – | |
|
|
414
|
+
| `accessibilityLabel`, `closeAccessibilityLabel` | string | generated | |
|
|
415
|
+
| `onShow(id)` | function | – | Called when the toast becomes visible (after any time in the queue) |
|
|
416
|
+
| `onPress({id,type,hide})` | function | – | Toast body pressed |
|
|
417
|
+
| `onAction({id,type,hide})` | function | – | Action button pressed |
|
|
418
|
+
| `onDismiss(reason, id)` | function | – | See [Callbacks](#callbacks) |
|
|
419
|
+
|
|
420
|
+
## Provider props reference
|
|
421
|
+
|
|
422
|
+
Defaults set on the provider apply to every toast that does not override them. Changing a provider prop affects toasts shown afterwards. Invalid values throw a `TypeError`.
|
|
423
|
+
|
|
424
|
+
| Prop | Type | Default | Description |
|
|
425
|
+
| ------------------------------------------------------ | ------------------------------------------------ | -------------------- | --------------------------------------------- |
|
|
426
|
+
| `theme` | `'light' \| 'dark' \| 'system' \| object` | `'light'` | See [Themes](#themes) |
|
|
427
|
+
| `position` | position | `'top'` | Default position |
|
|
428
|
+
| `duration` | number (ms) | `3000` | Default duration |
|
|
429
|
+
| `animation` | animation | `'slide'` | |
|
|
430
|
+
| `animationDuration` | number (ms) | `250` | |
|
|
431
|
+
| `maxVisible` | integer >= 1 | `3` | Toasts on screen at once; the rest are queued |
|
|
432
|
+
| `closeButton`, `swipeToDismiss`, `progress`, `haptics` | boolean | see options | Defaults for every toast |
|
|
433
|
+
| `titles` | `{ success?, error?, warning?, info?, custom? }` | – | Default title per type; `''` hides it |
|
|
434
|
+
| `spacing` | number | theme `spacing` (10) | Gap between stacked toasts |
|
|
435
|
+
| `offset` | `{ x?, y? }` | `{ x: 0, y: 0 }` | Extra distance from the screen edges |
|
|
436
|
+
| `insets` | `{ top?, bottom?, left?, right? }` | automatic | Overrides the detected safe-area insets |
|
|
437
|
+
| `global` | boolean | `false` | Registers this provider for the `Toast` API |
|
|
438
|
+
| `style` | style | `{ flex: 1 }` | Style of the wrapper `View` |
|
|
439
|
+
|
|
440
|
+
## Callbacks
|
|
441
|
+
|
|
442
|
+
- `onDismiss(reason, id)` is called **exactly once**, at the moment dismissal starts (before the exit animation), for visible toasts and for toasts cancelled while still queued. `reason` is one of `'timeout' | 'manual' | 'swipe' | 'close' | 'action' | 'press' | 'hideAll'`.
|
|
443
|
+
- `onAction` fires once when `dismissOnAction` is `true` (default) even if the button is tapped repeatedly; with `dismissOnAction: false` it fires on every press.
|
|
444
|
+
- Exceptions thrown inside callbacks are caught and logged with `console.error`; they cannot corrupt the queue.
|
|
445
|
+
|
|
446
|
+
## Queue behaviour
|
|
447
|
+
|
|
448
|
+
- At most `maxVisible` toasts are on screen; further requests wait in a **FIFO** queue.
|
|
449
|
+
- A toast occupies its slot until its **exit animation has finished**, then the oldest queued toast is promoted. Its timer starts when it becomes visible, not when it was requested.
|
|
450
|
+
- `hide(id)` also cancels a queued toast; `hideAll()` clears the queue and dismisses visible toasts.
|
|
451
|
+
- Raising `maxVisible` at runtime promotes queued toasts immediately.
|
|
452
|
+
- Generated ids are unique and never collide with ids you pass.
|
|
453
|
+
|
|
454
|
+
## Themes
|
|
455
|
+
|
|
456
|
+
```jsx
|
|
457
|
+
import { ToastProvider, createTheme } from 'react-native-toast-message-library';
|
|
458
|
+
|
|
459
|
+
<ToastProvider theme="dark" /> // 'light' | 'dark' | 'system'
|
|
460
|
+
|
|
461
|
+
<ToastProvider
|
|
462
|
+
theme={createTheme('dark', {
|
|
463
|
+
borderRadius: 4,
|
|
464
|
+
typography: { fontFamily: 'Inter', titleSize: 16 },
|
|
465
|
+
colors: { success: { background: '#052E1B', text: '#fff' } },
|
|
466
|
+
})}
|
|
467
|
+
/>
|
|
468
|
+
|
|
469
|
+
<ToastProvider theme={{ extends: 'dark', padding: 20 }} /> // a plain object also works
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
A theme object is deep-merged over the light theme (or the one named by `extends`). Every key (`colors.<type>.{background,text,border,icon,iconText}`, `borderRadius`, `borderWidth`, `padding`, `spacing`, `iconSize`, `typography`, `shadow`, `actionColor`, `closeColor`, `progressTrack`) is listed in `src/themes/light.js`.
|
|
473
|
+
|
|
474
|
+
## Custom icons and content
|
|
475
|
+
|
|
476
|
+
```jsx
|
|
477
|
+
toast.show({
|
|
478
|
+
type: 'custom',
|
|
479
|
+
message: 'Deployed',
|
|
480
|
+
icon: ({ color, size }) => <RocketIcon color={color} size={size} />,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
toast.success('No icon here', { icon: false });
|
|
484
|
+
|
|
485
|
+
toast.show({
|
|
486
|
+
type: 'custom',
|
|
487
|
+
render: ({ hide, colors }) => (
|
|
488
|
+
<Text style={{ color: colors.text }} onPress={hide}>
|
|
489
|
+
Fully custom: tap to close
|
|
490
|
+
</Text>
|
|
491
|
+
),
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Action buttons
|
|
496
|
+
|
|
497
|
+
```jsx
|
|
498
|
+
toast.show({
|
|
499
|
+
type: 'success',
|
|
500
|
+
title: 'Message deleted',
|
|
501
|
+
duration: 6000,
|
|
502
|
+
action: { label: 'Undo' },
|
|
503
|
+
onAction: () => restoreMessage(),
|
|
504
|
+
onDismiss: reason => reason === 'timeout' && permanentlyDelete(),
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Animations and reduced motion
|
|
509
|
+
|
|
510
|
+
All animations animate only `opacity` and `transform`, so they use the native driver. The progress bar animates `width` (a layout property) and therefore runs on the JS thread; it is a 3 dp element. When the system "reduce motion" setting is on, `slide` and `zoom` are replaced by `fade`; `none` stays `none`. After a toast exits, toasts stacked next to it move into place without animation.
|
|
511
|
+
|
|
512
|
+
## Accessibility
|
|
513
|
+
|
|
514
|
+
- Each toast is an `alert` (or a `button` when `onPress` is set) with a label such as `"Error. Disk full"`, so the type is available to screen readers and not only conveyed by colour or icon. The decorative icon glyph is hidden from assistive technology.
|
|
515
|
+
- Android: `accessibilityLiveRegion="polite"`. iOS: announcements via `announceForAccessibility`, throttled to one per 1.5 s so a burst of queued toasts does not interrupt VoiceOver repeatedly.
|
|
516
|
+
- Close and action buttons are labelled and have touch targets of at least 44 dp (including `hitSlop`).
|
|
517
|
+
- Text scales with the system font size up to 1.6×.
|
|
518
|
+
|
|
519
|
+
**Not covered:** the screen-reader behaviour follows platform guidelines but has not been tested on real TalkBack / VoiceOver. Toasts that disappear on a timer can be missed by assistive-technology users; use `duration: 0` plus a close button for important messages.
|
|
520
|
+
|
|
521
|
+
## TypeScript
|
|
522
|
+
|
|
523
|
+
Declarations ship in `types/index.d.ts` (checked by `npm run typecheck`).
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
import {
|
|
527
|
+
useToast,
|
|
528
|
+
type ToastOptions,
|
|
529
|
+
} from 'react-native-toast-message-library';
|
|
530
|
+
|
|
531
|
+
const opts: ToastOptions = {
|
|
532
|
+
type: 'info',
|
|
533
|
+
title: 'Hello',
|
|
534
|
+
description: 'World',
|
|
535
|
+
position: 'bottom-right',
|
|
536
|
+
offsetY: 12,
|
|
537
|
+
};
|
|
538
|
+
useToast().show(opts);
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Exported types include `ToastOptions`, `ToastApi`, `ToastProviderProps`, `ToastTitles`, `ToastTheme`, `ToastThemeOverrides`, `ToastType`, `ToastPosition`, `ToastAnimation`, `ToastDismissReason` and `Insets`.
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## Recipes (copy and paste examples)
|
|
546
|
+
|
|
547
|
+
### 1. Result of an API call
|
|
548
|
+
|
|
549
|
+
```js
|
|
550
|
+
async function save() {
|
|
551
|
+
try {
|
|
552
|
+
await api.save();
|
|
553
|
+
toast.success({ title: 'Saved', description: 'Your changes are live.' });
|
|
554
|
+
} catch (error) {
|
|
555
|
+
toast.error(error.message, { title: 'Could not save', duration: 5000 });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### 2. Loading → success / error with one toast
|
|
561
|
+
|
|
562
|
+
```js
|
|
563
|
+
const ID = 'upload';
|
|
564
|
+
toast.show({
|
|
565
|
+
id: ID,
|
|
566
|
+
type: 'info',
|
|
567
|
+
title: 'Uploading…',
|
|
568
|
+
duration: 0,
|
|
569
|
+
closeButton: false,
|
|
570
|
+
});
|
|
571
|
+
try {
|
|
572
|
+
await upload();
|
|
573
|
+
toast.show({
|
|
574
|
+
id: ID,
|
|
575
|
+
type: 'success',
|
|
576
|
+
title: 'Uploaded',
|
|
577
|
+
description: 'Your file is ready.',
|
|
578
|
+
});
|
|
579
|
+
} catch {
|
|
580
|
+
toast.show({
|
|
581
|
+
id: ID,
|
|
582
|
+
type: 'error',
|
|
583
|
+
title: 'Upload failed',
|
|
584
|
+
duration: 0,
|
|
585
|
+
closeButton: true,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### 3. Undo
|
|
591
|
+
|
|
592
|
+
```js
|
|
593
|
+
toast.show({
|
|
594
|
+
type: 'success',
|
|
595
|
+
title: 'Item deleted',
|
|
596
|
+
action: { label: 'Undo' },
|
|
597
|
+
duration: 6000,
|
|
598
|
+
onAction: restoreItem,
|
|
599
|
+
onDismiss: reason => reason === 'timeout' && deleteForever(),
|
|
600
|
+
});
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### 4. Toast from an API client or Redux middleware (outside React)
|
|
604
|
+
|
|
605
|
+
```jsx
|
|
606
|
+
<ToastProvider global>…</ToastProvider>
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
```js
|
|
610
|
+
// api.js
|
|
611
|
+
import { Toast } from 'react-native-toast-message-library';
|
|
612
|
+
|
|
613
|
+
axios.interceptors.response.use(undefined, error => {
|
|
614
|
+
Toast.error(error.message, { title: 'Network error' });
|
|
615
|
+
return Promise.reject(error);
|
|
616
|
+
});
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### 5. Translate or remove all default titles
|
|
620
|
+
|
|
621
|
+
```jsx
|
|
622
|
+
<ToastProvider titles={{ success: 'સફળ', error: 'ભૂલ', warning: 'ચેતવણી', info: 'માહિતી' }} />
|
|
623
|
+
<ToastProvider titles={{ success: '', error: '', warning: '', info: '' }} /> {/* no titles */}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### 6. Bottom toasts that avoid the home indicator and a tab bar
|
|
627
|
+
|
|
628
|
+
```jsx
|
|
629
|
+
<SafeAreaProvider>
|
|
630
|
+
<ToastProvider position="bottom" offset={{ y: 56 }}>
|
|
631
|
+
…
|
|
632
|
+
</ToastProvider>
|
|
633
|
+
</SafeAreaProvider>
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### 7. Persistent error with a close button and a retry action
|
|
637
|
+
|
|
638
|
+
```js
|
|
639
|
+
toast.error('The server is not reachable.', {
|
|
640
|
+
title: 'Offline',
|
|
641
|
+
duration: 0,
|
|
642
|
+
closeButton: true,
|
|
643
|
+
action: { label: 'Retry' },
|
|
644
|
+
dismissOnAction: false,
|
|
645
|
+
onAction: ({ hide }) => retry().then(hide),
|
|
646
|
+
});
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### 8. Brand-coloured toast
|
|
650
|
+
|
|
651
|
+
```js
|
|
652
|
+
toast.show({
|
|
653
|
+
type: 'custom',
|
|
654
|
+
title: 'Welcome back',
|
|
655
|
+
description: 'Good to see you again.',
|
|
656
|
+
backgroundColor: '#1E1B4B',
|
|
657
|
+
textColor: '#EEF2FF',
|
|
658
|
+
borderColor: '#818CF8',
|
|
659
|
+
iconColor: '#C7D2FE',
|
|
660
|
+
icon: ({ color }) => <Text style={{ color, fontSize: 22 }}>👋</Text>,
|
|
661
|
+
});
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### 9. Theme follows the device
|
|
665
|
+
|
|
666
|
+
```jsx
|
|
667
|
+
<ToastProvider theme="system">…</ToastProvider>
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
## Troubleshooting
|
|
673
|
+
|
|
674
|
+
| Symptom | Cause / fix |
|
|
675
|
+
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
676
|
+
| `useToast() was called outside of a <ToastProvider>` | Render the component below the provider (usually in `App.js`). |
|
|
677
|
+
| `Toast.* was called but no <ToastProvider global> is mounted` | Add the `global` prop, or use `useToast()`. |
|
|
678
|
+
| `Invalid position "…"` or another `TypeError` | Fix the option or prop; the message lists the allowed values. |
|
|
679
|
+
| `the toast would be empty` | Give the toast a `message`/`description`, a non-empty `title` or a `render` function. |
|
|
680
|
+
| `use either "message" or "description"` | Pass only one of them. |
|
|
681
|
+
| Toast hidden behind a modal / native screen | See [Limitations](#limitations). |
|
|
682
|
+
| Toast under the notch / status bar | Install `react-native-safe-area-context` and use `SafeAreaProvider`, or pass `insets`. |
|
|
683
|
+
| Toast too close to the header or tab bar | Use `offset={{ y }}` on the provider or `offsetY` on the toast. |
|
|
684
|
+
| Bottom toast hidden by the keyboard | Use `offsetY` or show it at `top`. |
|
|
685
|
+
| Nothing vibrates with `haptics` | Android needs `<uses-permission android:name="android.permission.VIBRATE" />`. |
|
|
686
|
+
| Layout broken because of the wrapper View | Pass `style` to the provider. |
|
|
687
|
+
|
|
688
|
+
## Limitations
|
|
689
|
+
|
|
690
|
+
- Toasts render inside the React tree. They appear **under** React Native `Modal`s and native-presented screens (for example native-stack modal presentations). Render another `ToastProvider` inside such a modal.
|
|
691
|
+
- The fallback insets (used without `react-native-safe-area-context`) are approximations. Install the library for exact values.
|
|
692
|
+
- The keyboard is not tracked automatically (see the offsets section).
|
|
693
|
+
- `center` toasts ignore `offsetY`.
|
|
694
|
+
- `haptics` uses the built-in `Vibration` API: Android honours short vibrations (needs the `VIBRATE` permission), iOS ignores the duration and vibrates for a fixed ~0.4 s. It is not a Taptic Engine integration.
|
|
695
|
+
- Swipe-to-dismiss uses `PanResponder` (horizontal swipes only). It is implemented but **not covered by automated tests** (gesture simulation is not available in the Jest setup); verify it on a device.
|
|
696
|
+
- Visual rendering, animation smoothness and screen-reader behaviour have not been verified on a device or emulator.
|
|
697
|
+
- Toasts next to a removed toast jump into position (no layout animation).
|
|
698
|
+
- `onDismiss` fires when dismissal starts, not when the exit animation ends.
|
|
699
|
+
|
|
700
|
+
## Development
|
|
701
|
+
|
|
702
|
+
The repository root is both the library and a React Native CLI demo app (no Expo). Node.js 22.11 or newer is needed for the demo app's toolchain (React Native 0.88); the published library itself has no Node requirement.
|
|
703
|
+
|
|
704
|
+
```sh
|
|
705
|
+
npm install
|
|
706
|
+
npm test # Jest
|
|
707
|
+
npm run lint
|
|
708
|
+
npm run typecheck
|
|
709
|
+
npm pack --dry-run # inspect the published file list
|
|
710
|
+
|
|
711
|
+
npm start # Metro (terminal 1)
|
|
712
|
+
npm run android # terminal 2; needs Android SDK + JDK
|
|
713
|
+
npm run ios # macOS + Xcode; cd ios && bundle exec pod install first
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
Layout: `src/` (the library: `manager` = queue + timers, `components`, `animations`, `themes`, `utils`), `types/` (TypeScript declarations), `__tests__/`, and `App.js` + `demo/` (the demo app).
|
|
717
|
+
|
|
718
|
+
## Publishing
|
|
719
|
+
|
|
720
|
+
Current release: **1.0.0**. Before the first publish of a new version:
|
|
721
|
+
|
|
722
|
+
```sh
|
|
723
|
+
# 1. Update CHANGELOG.md (new version heading + date) and the "version" in package.json
|
|
724
|
+
npm ci
|
|
725
|
+
npm run validate # lint + typecheck + tests + npm pack --dry-run
|
|
726
|
+
npm pack --dry-run # should list only src/, types/index.d.ts, README.md, CHANGELOG.md, LICENSE, package.json
|
|
727
|
+
|
|
728
|
+
# 2. Publish
|
|
729
|
+
npm login # interactive; never commit tokens
|
|
730
|
+
npm publish # publishConfig.access is already "public"
|
|
731
|
+
|
|
732
|
+
# 3. Verify, tag and push
|
|
733
|
+
npm view react-native-toast-message-library version
|
|
734
|
+
git tag v1.0.0 && git push origin v1.0.0
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
For every later release (semantic versioning: patch = fixes, minor = backwards-compatible features, major = breaking changes):
|
|
738
|
+
|
|
739
|
+
```sh
|
|
740
|
+
# update CHANGELOG.md first
|
|
741
|
+
npm version patch # or minor / major; bumps package.json and package-lock.json and creates the git tag
|
|
742
|
+
npm publish
|
|
743
|
+
git push --follow-tags
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
> An already published version can never be published again. If `npm publish` says "cannot publish over the previously published versions", bump the version first.
|
|
747
|
+
|
|
748
|
+
### Release checklist
|
|
749
|
+
|
|
750
|
+
- [ ] `npm run validate` passes
|
|
751
|
+
- [ ] `CHANGELOG.md` entry added and dated; `version` bumped in `package.json` (and `package-lock.json`)
|
|
752
|
+
- [ ] Demo app run on an Android device/emulator **and** an iOS simulator (swipe, queue, themes, insets, a11y spot check)
|
|
753
|
+
- [ ] `repository`, `bugs` and `homepage` fields are filled in `package.json` (they make the npm page link to your GitHub)
|
|
754
|
+
- [ ] `npm pack --dry-run` file list reviewed
|
|
755
|
+
- [ ] Published, verified with `npm view`, tag pushed
|
|
756
|
+
|
|
757
|
+
## Contributing
|
|
758
|
+
|
|
759
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
760
|
+
|
|
761
|
+
## License
|
|
762
|
+
|
|
763
|
+
MIT © Denish Dabhi
|