feat(react): Toast component with Toaster and imperative toast() API#1190
Conversation
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Auto-dismiss effect cleanup cancels exit animation timer
- Introduced separate refs for auto-dismiss and exit cleanup timers to prevent the effect cleanup from clearing the exit timer.
- ✅ Fixed: Raw animation duration instead of design token
- Replaced the hardcoded 200ms with AnimationDuration.Regularly from @metamask/design-tokens.
- ✅ Fixed: Missing requestAnimationFrame cleanup enables stale visibility update
- Captured rAF IDs and canceled them in the effect cleanup (and on close) to prevent stale visibility updates.
Or push these changes by commenting:
@cursor push 89e3e504a1
Preview (89e3e504a1)
diff --git a/packages/design-system-react/src/components/Toast/Toast.constants.ts b/packages/design-system-react/src/components/Toast/Toast.constants.ts
--- a/packages/design-system-react/src/components/Toast/Toast.constants.ts
+++ b/packages/design-system-react/src/components/Toast/Toast.constants.ts
@@ -1,10 +1,11 @@
import { ToastSeverity } from '@metamask/design-system-shared';
+import { AnimationDuration } from '@metamask/design-tokens';
import { IconColor, IconName } from '../../types';
export const TOAST_VISIBILITY_DURATION = 2750;
/** Duration of the enter/exit CSS transition in milliseconds. */
-export const TOAST_ANIMATION_DURATION = 200;
+export const TOAST_ANIMATION_DURATION = AnimationDuration.Regularly;
export const TOAST_SEVERITY_ICON_MAP = {
[ToastSeverity.Success]: {
diff --git a/packages/design-system-react/src/components/Toast/Toaster.tsx b/packages/design-system-react/src/components/Toast/Toaster.tsx
--- a/packages/design-system-react/src/components/Toast/Toaster.tsx
+++ b/packages/design-system-react/src/components/Toast/Toaster.tsx
@@ -48,21 +48,44 @@
const replacementTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
null,
);
- const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+ // Separate timers to avoid cleanup collisions.
+ const autoDismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
+ null,
+ );
+ const exitCleanupTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
+ null,
+ );
+ // Track pending rAF callbacks for enter animation.
+ const enterRafId1Ref = useRef<number | null>(null);
+ const enterRafId2Ref = useRef<number | null>(null);
const innerRef = useRef<ToasterRef | null>(null);
const closeToast = () => {
+ // Cancel any pending enter rAFs to prevent stale visibility updates.
+ if (enterRafId1Ref.current !== null) {
+ cancelAnimationFrame(enterRafId1Ref.current);
+ enterRafId1Ref.current = null;
+ }
+ if (enterRafId2Ref.current !== null) {
+ cancelAnimationFrame(enterRafId2Ref.current);
+ enterRafId2Ref.current = null;
+ }
if (replacementTimerRef.current !== null) {
clearTimeout(replacementTimerRef.current);
replacementTimerRef.current = null;
}
- if (dismissTimerRef.current !== null) {
- clearTimeout(dismissTimerRef.current);
- dismissTimerRef.current = null;
+ // Clear any auto-dismiss timer and any prior exit cleanup timer.
+ if (autoDismissTimerRef.current !== null) {
+ clearTimeout(autoDismissTimerRef.current);
+ autoDismissTimerRef.current = null;
}
+ if (exitCleanupTimerRef.current !== null) {
+ clearTimeout(exitCleanupTimerRef.current);
+ exitCleanupTimerRef.current = null;
+ }
setIsVisible(false);
- dismissTimerRef.current = setTimeout(() => {
- dismissTimerRef.current = null;
+ exitCleanupTimerRef.current = setTimeout(() => {
+ exitCleanupTimerRef.current = null;
setToastOptions(undefined);
}, TOAST_ANIMATION_DURATION);
};
@@ -73,10 +96,15 @@
if (toastOptions) {
setIsVisible(false);
- if (dismissTimerRef.current !== null) {
- clearTimeout(dismissTimerRef.current);
- dismissTimerRef.current = null;
+ // Clear any existing timers when replacing an in-flight toast.
+ if (autoDismissTimerRef.current !== null) {
+ clearTimeout(autoDismissTimerRef.current);
+ autoDismissTimerRef.current = null;
}
+ if (exitCleanupTimerRef.current !== null) {
+ clearTimeout(exitCleanupTimerRef.current);
+ exitCleanupTimerRef.current = null;
+ }
timeoutDuration = TOAST_ANIMATION_DURATION;
setToastOptions(undefined);
}
@@ -107,24 +135,36 @@
// Trigger enter animation after toast is mounted in the DOM.
useEffect(() => {
if (toastOptions && !isVisible) {
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
+ enterRafId1Ref.current = requestAnimationFrame(() => {
+ enterRafId2Ref.current = requestAnimationFrame(() => {
setIsVisible(true);
});
});
+ // Cleanup to cancel pending rAFs if toast is dismissed quickly.
+ return () => {
+ if (enterRafId1Ref.current !== null) {
+ cancelAnimationFrame(enterRafId1Ref.current);
+ enterRafId1Ref.current = null;
+ }
+ if (enterRafId2Ref.current !== null) {
+ cancelAnimationFrame(enterRafId2Ref.current);
+ enterRafId2Ref.current = null;
+ }
+ };
}
+ return undefined;
}, [toastOptions]); // intentionally omit isVisible — only react to new toast options
// Auto-dismiss timer.
useEffect(() => {
if (isVisible && toastOptions && !toastOptions.hasNoTimeout) {
- dismissTimerRef.current = setTimeout(() => {
- dismissTimerRef.current = null;
+ autoDismissTimerRef.current = setTimeout(() => {
+ autoDismissTimerRef.current = null;
innerRef.current?.closeToast();
}, TOAST_VISIBILITY_DURATION);
return () => {
- if (dismissTimerRef.current !== null) {
- clearTimeout(dismissTimerRef.current);
+ if (autoDismissTimerRef.current !== null) {
+ clearTimeout(autoDismissTimerRef.current);
}
};
}You can send follow-ups to the cloud agent here.
📖 Storybook Preview |
📖 Storybook Preview |
📖 Storybook Preview |
📖 Storybook Preview |
6513f9c to
765c869
Compare
📖 Storybook Preview |
765c869 to
3e96075
Compare
📖 Storybook Preview |
3e96075 to
b598fe2
Compare
📖 Storybook Preview |
|
@metamaskbot publish-preview |
📖 Storybook Preview |
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
| export { Toast } from './Toast'; | ||
| export { Toaster, toast } from './Toaster'; | ||
| export { ToastSeverity } from './Toast.types'; | ||
| export { ToastSeverity } from '@metamask/design-system-shared'; |
There was a problem hiding this comment.
Moved ToastSeverity to shared
| /** | ||
| * Toast severity variants. | ||
| * `Default` renders no built-in leading icon. | ||
| */ | ||
| export const ToastSeverity = { | ||
| Default: 'default', | ||
| Success: 'success', | ||
| Warning: 'warning', | ||
| Danger: 'danger', | ||
| } as const; | ||
|
|
||
| export type ToastSeverity = (typeof ToastSeverity)[keyof typeof ToastSeverity]; |
There was a problem hiding this comment.
Moved ToastSeverity to shared as it is used by both React and React Native versions of the Toast
| export { | ||
| TOAST_VISIBILITY_DURATION, | ||
| TOAST_ANIMATION_DURATION, | ||
| TOAST_BOTTOM_PADDING, | ||
| } from './Toast.constants'; | ||
| export { TOAST_BOTTOM_PADDING } from './Toast.constants'; |
There was a problem hiding this comment.
Moved TOAST_VISIBILITY_DURATION and TOAST_ANIMATION_DURATION to shared as they are used by both React Native and React versions of the Toast
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Animation duration silently changed from 300ms to 200ms
- Updated shared TOAST_ANIMATION_DURATION to AnimationDuration.Regularly (300ms) to match prior React Native behavior.
Or push these changes by commenting:
@cursor push 583134f2fc
Preview (583134f2fc)
diff --git a/packages/design-system-shared/src/types/Toast/Toast.constants.ts b/packages/design-system-shared/src/types/Toast/Toast.constants.ts
--- a/packages/design-system-shared/src/types/Toast/Toast.constants.ts
+++ b/packages/design-system-shared/src/types/Toast/Toast.constants.ts
@@ -4,4 +4,4 @@
* Shared toast timing constants.
*/
export const TOAST_VISIBILITY_DURATION = 2750;
-export const TOAST_ANIMATION_DURATION = AnimationDuration.Promptly;
+export const TOAST_ANIMATION_DURATION = AnimationDuration.Regularly;You can send follow-ups to the cloud agent here.
| }; | ||
| ``` | ||
|
|
||
| `<Toaster />` must be rendered exactly once. On mount it registers the `toast(...)` / `toast.dismiss()` API so it can be called from anywhere in your app. |
There was a problem hiding this comment.
This README is primarily documenting the toast() API because that is the consumer entry point. The inline <Toast /> examples stay here to make the shape easier to scan, but the code snippets are the canonical usage for the imperative flow.
| } | ||
| : undefined; | ||
|
|
||
| // Toast reuses BannerBase so the web surface stays aligned with the shared |
There was a problem hiding this comment.
Toast reuses BannerBase so the web surface stays aligned with the shared banner layout and the React Native Toast API. That keeps the consumer contract consistent even though the platform primitives differ.
| export default meta; | ||
| type Story = StoryObj<ToastProps>; | ||
|
|
||
| export const Default: Story = { |
There was a problem hiding this comment.
These stories intentionally render Toast directly where that makes the component easier to review in Storybook. The imperative toast(...) flow is still covered by the default story, so this keeps the docs focused without duplicating the click-to-show plumbing everywhere.
|
|
||
| import { IconColor, IconName } from '../../types'; | ||
|
|
||
| // TODO: Replace this map with a web IconAlert component once the shared |
There was a problem hiding this comment.
TOAST_SEVERITY_ICON_MAP is a temporary web-only bridge until React has an IconAlert equivalent. Keeping it local avoids leaking a platform-specific icon abstraction into shared code.
There was a problem hiding this comment.
Just wanted to write a comment that we could unify the codebase 😁 But yes, your comment explains why we made it like this 👍
| TOAST_BOTTOM_PADDING, | ||
| TOAST_VISIBILITY_DURATION, | ||
| } from './Toast.constants'; | ||
| import { TOAST_BOTTOM_PADDING } from './Toast.constants'; |
There was a problem hiding this comment.
TOAST_BOTTOM_PADDING is RN specific so leaving it in constants
|
|
||
| let registeredRef: RefObject<ToasterRef | null> | null = null; | ||
|
|
||
| const assertRegisteredRef = (method: 'dismiss' | 'toast'): ToasterRef => { |
There was a problem hiding this comment.
Calling toast() before <Toaster /> mounts throws immediately instead of silently no-oping. That gives consumers an explicit integration error instead of hiding a missing root provider. https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/core/store.ts
| }, TOAST_ANIMATION_DURATION); | ||
| }; | ||
|
|
||
| // Replace the currently mounted toast rather than queueing multiple toasts. |
There was a problem hiding this comment.
This intentionally replaces the current toast instead of queueing multiple items. It matches the current single-toast service model, and the same host can be extended to stack later without changing the consumer API. https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/core/store.ts https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/components/toaster.tsx
| }; | ||
| }, []); | ||
|
|
||
| // Delay the enter transition until after mount so the DOM can paint the |
There was a problem hiding this comment.
The double requestAnimationFrame and the separate auto-dismiss timer are doing the same job a toast viewport does in libraries like react-hot-toast: mount offscreen, animate in, keep visible for a fixed duration, then clear after exit. The outer container owns placement and lifecycle while Toast stays presentational. https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/components/toaster.tsx https://react-hot-toast.com/
| ToasterRef, | ||
| } from './Toast.types'; | ||
|
|
||
| let registeredRef: RefObject<ToasterRef | null> | null = null; |
There was a problem hiding this comment.
This follows the same mounted-host pattern used by react-hot-toast: a single Toaster instance registers the imperative API once, and callers interact with that host instead of managing toast state directly. https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/core/store.ts https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/components/toaster.tsx
|
|
||
| const ToasterComponent = forwardRef<ToasterRef, ToasterProps>( | ||
| ({ className, ...props }, ref) => { | ||
| const [toastOptions, setToastOptions] = useState<ToastOptions | undefined>( |
There was a problem hiding this comment.
The state is split into payload, visibility, replacement, auto-dismiss, exit cleanup, and enter animation refs so each phase can be controlled independently. That separation is what keeps dismiss and replacement from racing each other. https://raw.githubusercontent.com/timolins/react-hot-toast/main/src/components/toaster.tsx
bd7ac0f to
1bb48d7
Compare
| TOAST_VISIBILITY_DURATION, | ||
| } from './Toast.constants'; | ||
|
|
||
| describe('Toast.constants', () => { |
There was a problem hiding this comment.
This test is needed because design-system-shared enforces 100% coverage on runtime exports, and a new constants module would otherwise drop the package below threshold. Keeping the assertion here preserves the gate without weakening coverage rules.
📖 Storybook Preview |
📖 Storybook Preview |
📖 Storybook Preview |
📖 Storybook Preview |
📖 Storybook Preview |
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
| if (startAccessory !== null && startAccessory !== undefined) { | ||
| return startAccessory; | ||
| } | ||
|
|
||
| if (!severity || severity === ToastSeverity.Default) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const { name, color } = TOAST_SEVERITY_ICON_MAP[severity]; |
There was a problem hiding this comment.
That logic is so similar to what we have in mobile. Just thought that maybe this code can be unified somehow?
| if (!registeredRef?.current) { | ||
| const invocation = method === 'toast' ? 'toast()' : `toast.${method}()`; | ||
| throw new Error( | ||
| `${invocation} called before <Toaster /> mounted. Render <Toaster /> once at the root of your app.`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Same here, this code is copied/pasted. I think it's not blocking for this PR and we can merge it as is.
I'm just thinking whether it makes sense to create something like design-system-toaster/core or anything like that where we can put shared codebase for Toast?
Or you think that it may be an overkill, because we have different types anyway and we'll jsut get a more complex codebase because of that that will be harder to maintain?
| }, timeoutDuration); | ||
| }; | ||
|
|
||
| innerRef.current = { closeToast, showToast }; |
There was a problem hiding this comment.
I think assigning ref in the renderer will break react compiler rules. But I checked and the same code is present in react-native package too 🙈
I think it's fine for now, but we definitely need to make an audit (maybe add react compiler esling plugin) to spot all these violations automatically 🤞
Again, not blocking for these changes 👍
| borderWidth={1} | ||
| className={twMerge('rounded-xl', className)} | ||
| closeButtonProps={resolvedCloseButtonProps} | ||
| onClose={onClose ? () => onClose() : undefined} |
There was a problem hiding this comment.
Can this be simplifed to onClose={onClose}?
Theoretically it can be simplified to onClose={onClose ? onClose : undefined}, then to onClose={onClose ?? undefined}, but onClose already function or undefined, so theoretically it can be onClose={onClose}?
There was a problem hiding this comment.
Great catch! Missed this is self review thank you!
|
|
||
| import { IconColor, IconName } from '../../types'; | ||
|
|
||
| // TODO: Replace this map with a web IconAlert component once the shared |
There was a problem hiding this comment.
Just wanted to write a comment that we could unify the codebase 😁 But yes, your comment explains why we made it like this 👍
|
Also, do we need to prepare migration guide? |
Thanks for the thorough review @kirillzyusko! Will address your great suggestions in a follow up PR |
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Danger toast icon mismatches RN
- Changed web Danger toast icon to IconName.Error to align with RN’s IconAlertSeverity.Error.
Or push these changes by commenting:
@cursor push 4d3a6f56cd
You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 920c661. Configure here.
| [ToastSeverity.Danger]: { | ||
| name: IconName.Danger, | ||
| color: IconColor.ErrorDefault, | ||
| }, |
There was a problem hiding this comment.
Danger toast icon mismatches RN
Low Severity
For ToastSeverity.Danger, the web map uses IconName.Danger, while React Native maps danger to IconAlertSeverity.Error, which renders the error icon—so danger toasts can look different across platforms.
Triggered by learned rule: Align component API shapes across web and RN platforms
Reviewed by Cursor Bugbot for commit 920c661. Configure here.
## Release 44.0.0 This release adds `Toast`/`Toaster` and `Tag` to React, `ListItem` to React Native, aligns the `TextButton` API across platforms, and standardizes severity vocabulary (`Error` → `Danger`) across `AvatarIcon`, `IconAlert`, and `Tag`. ### 📦 Package Versions - `@metamask/design-system-shared`: **0.22.0** - `@metamask/design-system-react`: **0.26.0** - `@metamask/design-system-react-native`: **0.29.0** ### 🔄 Shared Type Updates (0.22.0) #### New shared types (#1190, #1203, #1224, #1225) **What Changed:** - Added `ToastPropsShared` and `ToastSeverity` for cross-platform `Toast` support - Added `ListItemPropsShared` and related types for cross-platform `ListItem` support - Added `TextButtonPropsShared` to align `TextButton` API across React and React Native - Added `AvatarNetworkSize` as a named export from the shared package **Impact:** - Enables consistent `Toast` and `ListItem` implementations across both platforms - Continues ADR-0003/0004 const-object + string-union pattern adoption #### Severity vocabulary: `.Error` → `.Danger` ([#1159](#1159)) **What Changed:** - `AvatarIconSeverity.Error` → `AvatarIconSeverity.Danger` - `IconAlertSeverity.Error` → `IconAlertSeverity.Danger` - `TagSeverity.Error` → `TagSeverity.Danger` **Impact:** - Breaking change for any consumer using `.Error` on these three const objects. Rendered colors are unchanged. ### 🌐 React Web Updates (0.26.0) #### Added - Added `Tag` component for categorization and filtering labels ([#1211](#1211)) - Added `Toast` component with `Toaster` provider and imperative `toast()` API ([#1190](#1190)) #### Changed - **BREAKING:** `TextButton` API aligned with React Native — `size`/`TextButtonSize` replaced by `variant`/`TextVariant`; `isInverse`, `isDisabled`, `textProps`, and icon/accessory props removed; `asChild` added ([#1224](#1224)) - **BREAKING:** `AvatarIconSeverity.Error` → `AvatarIconSeverity.Danger` ([#1159](#1159)) #### Fixed - Fixed `Toast` to support `toast()` calls made before `Toaster` mounts ([#1217](#1217)) ### 📱 React Native Updates (0.29.0) #### Added - Added `ListItem` component for list row layouts ([#1203](#1203)) - Added `Toast` component with `Toaster` provider and imperative `toast()` API ([#1190](#1190)) #### Changed - **BREAKING:** `AvatarIconSeverity.Error`, `IconAlertSeverity.Error`, and `TagSeverity.Error` → `.Danger` ([#1159](#1159)) ###⚠️ Breaking Changes #### TextButton rewrite (React Web Only) **What Changed:** - `size` / `TextButtonSize` removed — use `variant` / `TextVariant` instead - `isInverse`, `isDisabled`, `textProps`, start/end icons, and accessory slots removed - `asChild` added for semantic link composition **Migration:** ```tsx // Before (0.25.0) import { TextButton, TextButtonSize } from '@metamask/design-system-react'; <TextButton size={TextButtonSize.BodySm}>Learn more</TextButton> // After (0.26.0) import { TextButton } from '@metamask/design-system-react'; import { TextVariant } from '@metamask/design-system-shared'; <TextButton variant={TextVariant.BodySm}>Learn more</TextButton> ``` See [React Migration Guide](./packages/design-system-react/MIGRATION.md#from-version-0250-to-0260) #### Severity vocabulary: `.Error` → `.Danger` (Both Platforms) **What Changed:** - `AvatarIconSeverity.Error` → `AvatarIconSeverity.Danger` (React + React Native) - `IconAlertSeverity.Error` → `IconAlertSeverity.Danger` (React Native) - `TagSeverity.Error` → `TagSeverity.Danger` (React Native) **Migration:** ```tsx // Before <AvatarIcon severity={AvatarIconSeverity.Error} /> <IconAlert severity={IconAlertSeverity.Error} /> <Tag severity={TagSeverity.Error}>High risk</Tag> // After <AvatarIcon severity={AvatarIconSeverity.Danger} /> <IconAlert severity={IconAlertSeverity.Danger} /> <Tag severity={TagSeverity.Danger}>High risk</Tag> ``` See migration guides for complete instructions: - [React Migration Guide](./packages/design-system-react/MIGRATION.md#from-version-0250-to-0260) - [React Native Migration Guide](./packages/design-system-react-native/MIGRATION.md#from-version-0280-to-0290) ### ✅ Checklist - [x] Changelogs updated with human-readable descriptions - [x] Changelog validation passed (`yarn changelog:validate`) - [x] Version bumps follow semantic versioning - [x] design-system-shared: minor (0.21.0 → 0.22.0) - new shared types + breaking severity rename - [x] design-system-react: minor (0.25.0 → 0.26.0) - new components + breaking API changes - [x] design-system-react-native: minor (0.28.0 → 0.29.0) - new components + breaking severity rename - [x] Breaking changes documented with migration guidance - [x] Migration guides updated with before/after examples - [x] PR references included in changelog entries ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) - [x] I've reviewed the [Release Workflow](./.cursor/rules/release-workflow.md) cursor rule - [ ] All tests pass (`yarn build && yarn test && yarn lint`) - [x] Changelog validation passes (`yarn changelog:validate`) ## **Pre-merge reviewer checklist** - [ ] I've reviewed the [Reviewing Release PRs](./docs/reviewing-release-prs.md) guide - [ ] Package versions follow semantic versioning - [ ] Changelog entries are consumer-facing (not commit message regurgitation) - [ ] Breaking changes are documented in MIGRATION.md with examples - [ ] All unreleased changes are accounted for in changelogs <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > The release documents multiple breaking consumer APIs (TextButton on web, severity `.Error`→`.Danger`); upgrading without following MIGRATION.md will cause compile/runtime mismatches, though this PR does not change component implementation itself. > > **Overview** > **Release 44.0.0** cuts new versions of the design-system packages and records what shipped since the last release: monorepo root **43.0.0 → 44.0.0**, `@metamask/design-system-shared` **0.22.0**, `@metamask/design-system-react` **0.26.0**, and `@metamask/design-system-react-native` **0.29.0**. > > Changelogs document **React** additions (`Tag`, `Toast`/`Toaster`/`toast()`), a **breaking** `TextButton` rewrite (`size`/`TextButtonSize` → `variant`/`TextVariant`, dropped inverse/disabled/icons, added `asChild`), and **`AvatarIconSeverity.Error` → `.Danger`**. **React Native** adds **`ListItem`** and the same **severity rename** on `AvatarIcon`, `IconAlert`, and `Tag`. **Shared** adds cross-platform types (`ListItem`, `Toast`, `TextButton`, `AvatarNetworkSize`) and the shared **`.Error` → `.Danger`** severity vocabulary. > > **MIGRATION.md** on React and React Native gains **0.25→0.26** and **0.28→0.29** sections with before/after examples. The only non-release code change in the diff is reordering the `react-native-worklets` devDependency in `apps/storybook-react-native/package.json`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9c3345f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->



Description
Adds the Toast component pair for React: a presentational
Toast, a root-mountedToaster, and the imperativetoast()/toast.dismiss()API.The implementation keeps toast behavior aligned with React Native while staying web-native in its rendering and placement model. Shared toast severity and timing live in
@metamask/design-system-shared; platform-specific icon and layout concerns stay local.Related issues
Fixes: https://consensyssoftware.atlassian.net/browse/DSYS-514
Manual testing steps
Toastexamples render correctly.toast(...)from the docs example and confirm theToastermounts, animates in, auto-dismisses, and can be dismissed manually.Screenshots/Recordings
After
Toast component docs and stories
toast.after720.mov
Toast component covers extension use cases. Showing preview package Toast usage working in the extension
PR: MetaMask/metamask-extension#43122
toast.extension.loading720.mov
toast.extension.success720.mov
More toasts from extension click to expand image
Toast comparison between both platforms is aligned
toast.react.reactnative.720.mov
Pre-merge author checklist
Pre-merge reviewer checklist
Note
Medium Risk
New public imperative API with module-level registration and timer/animation lifecycle; risk is mostly integration misuse (e.g. missing Toaster), mitigated by tests and clear errors—not security-critical paths.
Overview
Adds React Toast support: a
Toastsurface onBannerBase, a rootToaster, andtoast()/toast.dismiss()for app-wide notifications with slide-up animation, auto-dismiss (~2750 ms), and single-toast replacement (no queue).ToastSeverityand timing constants (TOAST_ANIMATION_DURATION,TOAST_VISIBILITY_DURATION) move into@metamask/design-system-shared; React Native Toast re-exports those instead of defining them locally. Web keeps a local severity→icon map until a shared alert icon exists.Ships Storybook docs, package exports from
design-system-react, and broad unit tests for presentation, timers, replacement, unmount cleanup, and pre-mount errors.Reviewed by Cursor Bugbot for commit 920c661. Bugbot is set up for automated code reviews on this repo. Configure here.