Snackbar
Snackbars provide brief messages about app processes at the bottom of the screen.
Initialize Snackbar
An application should normally have a single Snackbar
added near the root of
the application.
import { CoreProviders } from "@react-md/core/CoreProviders";
+import { Snackbar } from "@react-md/core/snackbar/Snackbar";
import rmdConfig from "./rmdConfig.jsx";
return (
<CoreProviders {...rmdConfig}>
{children}
+ <Snackbar />
</CoreProviders>
);
Simple Toast
Once the Snackbar
component has been included in the application, toasts
can be added by using the addToast
function. Simple messages can be created
by providing the children
as the toast content.
The default behavior for toasts:
- Add a toast to the end of the queue
- Once a toast becomes visible, show it for 5 seconds
- Pause the timeout if the user hovers the toast
- Pause the timeout if the user blurs the window
- Hide the toast and display the next toast in the queue
"use client";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { type ReactElement } from "react";
export default function SimpleToastExample(): ReactElement {
return (
<Button
onClick={() => {
addToast({ children: "Hello, world!" });
}}
>
Add Toast
</Button>
);
}
Multiple Lines
The children
for the toast can also be any renderable element and span
multiple lines.
"use client";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { type ReactElement } from "react";
export default function MultipleLinesToastExample(): ReactElement {
return (
<Button
onClick={() => {
addToast({
children: (
<>
<p>This a toast that has multiple lines.</p>
<p>Pretty exciting.</p>
</>
),
});
}}
>
Add Toast
</Button>
);
}
Toast Theme
The Toast
supports all the theme colors by setting the theme
.
"use client";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { type ReactElement } from "react";
export default function ToastThemeExample(): ReactElement {
return (
<>
<Button
onClick={() => {
addToast({ children: "Surface Theme (Default)", theme: "surface" });
}}
themeType="contained"
>
Surface
</Button>
<Button
onClick={() => {
addToast({ children: "Primary Theme", theme: "primary" });
}}
theme="primary"
themeType="contained"
>
Primary
</Button>
<Button
onClick={() => {
addToast({ children: "Secondary Theme", theme: "secondary" });
}}
theme="secondary"
themeType="contained"
>
Secondary
</Button>
<Button
onClick={() => {
addToast({ children: "Success Theme", theme: "success" });
}}
theme="success"
themeType="contained"
>
Success
</Button>
<Button
onClick={() => {
addToast({ children: "Warning Theme", theme: "warning" });
}}
theme="warning"
themeType="contained"
>
Warning
</Button>
<Button
onClick={() => {
addToast({ children: "Error Theme", theme: "error" });
}}
theme="error"
themeType="contained"
>
Error
</Button>
</>
);
}
Custom Toast Visible Time
If the default duration of 5 seconds does not work for a specific toast, set the
visibleTime
to a duration in milliseconds to wait before hiding the toast.
"use client";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { type ReactElement } from "react";
export default function CustomToastVisibleTimeExample(): ReactElement {
return (
<Button
onClick={() => {
addToast({
children: "Wait 3 seconds instead of 5",
visibleTime: 3000,
});
}}
>
Toast!
</Button>
);
}
Add a Close Button
If a toast should be able to be dismissed early, a close button can be added by
adding closeButton: true
when creating the toast. The close button will
default to having an aria-label="Close"
and using the
close icon.
"use client";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { type ReactElement } from "react";
export default function AddACloseButtonExample(): ReactElement {
return (
<Button
onClick={() => {
addToast({ children: "Message.", closeButton: true });
}}
>
Toast!
</Button>
);
}
Close Button Props
If custom props are required for the close button, provide closeButtonProps
instead.
"use client";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { type ReactElement } from "react";
export default function CloseButtonPropsExample(): ReactElement {
return (
<Button
onClick={() => {
addToast({
children: "Message.",
closeButtonProps: {
theme: "warning",
themeType: "outline",
buttonType: "text",
children: "Close",
},
});
}}
>
Toast!
</Button>
);
}
Actionable Toasts
A toast can be updated to have an action button appear to the right of the
message by providing an action
object. It should normally have a simple
onClick
event handler and children
to display in the button.
"use client";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { type ReactElement } from "react";
export default function ActionableToastExample(): ReactElement {
return (
<Button
onClick={() => {
addToast({
children: "This toast has an optional action",
action: {
children: "Undo",
onClick: () => {
// do something
},
},
});
}}
>
Toast!
</Button>
);
}
Action and Close Button
Both an action and close button can be displayed together.
"use client";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { type ReactElement } from "react";
export default function ActionAndCloseButtonExample(): ReactElement {
return (
<Button
onClick={() => {
addToast({
children: "Message.",
closeButton: true,
action: {
onClick: () => {
// do something,
},
children: "Undo",
},
});
}}
>
Toast!
</Button>
);
}
Require an action
If an action must be clicked to hide the toast, set the visibleTime
to
null
.
"use client";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { type ReactElement } from "react";
export default function RequireAnActionExample(): ReactElement {
return (
<Button
onClick={() => {
addToast({
children: "Message",
action: {
children: "Must click",
onClick: () => {
// do something
},
},
visibleTime: null,
});
}}
>
Toast!
</Button>
);
}
Custom Action Button
If the action is complex and requires additional behavior, provide an
actionButton
instead.
"use client";
import { AsyncButton } from "@react-md/core/button/AsyncButton";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { useCurrentToastActions } from "@react-md/core/snackbar/useCurrentToastActions";
import { wait } from "@react-md/core/utils/wait";
import { type ReactElement } from "react";
export default function CustomActionButtonExample(): ReactElement {
return (
<Button
onClick={() => {
addToast({
children: "Something happened.",
actionButton: <ActionButton />,
visibleTime: null,
});
}}
>
Toast!
</Button>
);
}
function ActionButton(): ReactElement {
const { removeToast } = useCurrentToastActions();
return (
<AsyncButton
onClick={async () => {
await wait(5000);
removeToast(true);
}}
theme="secondary"
>
Undo
</AsyncButton>
);
}
Stacked Toasts
If the action should be below the toast content instead of inline, enable the
stacked
option when creating a toast.
"use client";
import { Button } from "@react-md/core/button/Button";
import { addToast } from "@react-md/core/snackbar/ToastManager";
import { type ReactElement } from "react";
export default function StackedToastExample(): ReactElement {
return (
<>
<Button
onClick={() => {
addToast({
children: "Hello, world!",
stacked: true,
action: "Action",
});
}}
>
Stacked
</Button>
<Button
onClick={() => {
addToast({
children: "Hello, world!",
stacked: true,
action: "Action",
closeButton: true,
});
}}
>
With Close Button
</Button>
</>
);
}
Custom Toast Manager
If multiple Snackbar
need to be mounted at the same time, create a new
ToastManager
and wrap the React tree with the ToastManagerProvider
. Now
toasts should be created by calling manager.addToast
or the addToast
returned by useAddToast
.
This example will give a quick example and show how the Snackbar
can be
updated to have toastDefaults
.
Check out the custom toast renderer example to see a real use case for the custom toast manager.
"use client";
import { Button } from "@react-md/core/button/Button";
import { Snackbar } from "@react-md/core/snackbar/Snackbar";
import { ToastManager, addToast } from "@react-md/core/snackbar/ToastManager";
import {
ToastManagerProvider,
useAddToast,
} from "@react-md/core/snackbar/ToastManagerProvider";
import { type ReactElement } from "react";
const manager = new ToastManager();
export default function CustomToastManagerExample(): ReactElement {
return (
<ToastManagerProvider manager={manager}>
<Content />
<Snackbar
toastDefaults={{
theme: "secondary",
closeButton: true,
}}
/>
</ToastManagerProvider>
);
}
function Content(): ReactElement {
const currentAddToast = useAddToast();
return (
<>
<Button
onClick={() => {
currentAddToast({ children: "Context initiated toast." });
}}
>
Context Toast
</Button>
<Button
onClick={() => {
manager.addToast({ children: "Manager initiated toast." });
}}
>
Manager Toast
</Button>
<Button
onClick={() => {
addToast({ children: "Global toast.", closeButton: true });
}}
>
Global Toast
</Button>
</>
);
}
The following demos and features do not require a custom
ToastManager
to work outside of this documentation site unless specifically
stated.
Preventing Duplicate Toasts
The default behavior allows multiple toasts with the same content to be added
into the queue because each toast gets a unique toastId
generated by
nanoid when added into the queue. Since this
might not be ideal, toast timers can automatically restart if the same content
is added into the queue. Manually set the toastId
when adding a toast to
enable this feature.
Check out the custom toast renderer example
for another use case for manually setting the toastId
.
"use client";
import { AsyncButton } from "@react-md/core/button/AsyncButton";
import { Snackbar } from "@react-md/core/snackbar/Snackbar";
import { ToastManager } from "@react-md/core/snackbar/ToastManager";
import { ToastManagerProvider } from "@react-md/core/snackbar/ToastManagerProvider";
import { DEFAULT_SCALE_TIMEOUT } from "@react-md/core/transition/useScaleTransition";
import { type UseStateSetter } from "@react-md/core/types";
import { type ReactElement, useEffect, useState } from "react";
const TOAST_ID = "toast-id-1";
const manager = new ToastManager();
export default function PreventingDuplicateToastsExample(): ReactElement {
const [running, setRunning] = useMultipleToasts();
return (
<ToastManagerProvider manager={manager}>
<AsyncButton
loading={running}
onClick={() => {
setRunning(true);
manager.addToast({
toastId: TOAST_ID,
children: "Message 1",
});
}}
>
Toast!
</AsyncButton>
<Snackbar
toastDefaults={{
children: <ActiveTime />,
closeButton: true,
onExited: () => {
setRunning(false);
},
}}
/>
</ToastManagerProvider>
);
}
function useMultipleToasts(): [
running: boolean,
setRunning: UseStateSetter<boolean>,
] {
const [running, setRunning] = useState(false);
useEffect(() => {
if (!running) {
return;
}
let timeout = window.setTimeout(() => {
manager.addToast({
toastId: TOAST_ID,
children: "This will replace the content!",
});
timeout = window.setTimeout(() => {
// this will just reset the time
manager.addToast({ toastId: TOAST_ID });
timeout = window.setTimeout(() => {
manager.addToast({
toastId: TOAST_ID,
children: "Replacing again, but no restart",
duplicates: "update",
});
}, 3000);
}, 3000);
}, 3000);
return () => {
window.clearTimeout(timeout);
};
}, [running]);
return [running, setRunning];
}
function ActiveTime(): ReactElement {
const [time, setTime] = useState(0);
useEffect(() => {
let interval: number | undefined;
const timeout = window.setTimeout(() => {
interval = window.setInterval(() => {
setTime((prevTime) => prevTime + 1);
}, 1000);
}, DEFAULT_SCALE_TIMEOUT.enter);
return () => {
window.clearTimeout(timeout);
window.clearInterval(interval);
};
}, []);
return <div>{`Visible for ${time} seconds`}</div>;
}
Multiple Visible Toasts
The Snackbar
can be configured to display more than one toast at a time by
setting the limit
prop.
Snackbar Position
Set the position
prop on the Snackbar
to update the position within the
viewport to one of the following: "bottom"
(default), "bottom-left"
,
"bottom-right"
, "top"
, "top-left"
, or "top-right"
.
"use client";
import { Button } from "@react-md/core/button/Button";
import { Option } from "@react-md/core/form/Option";
import { Select } from "@react-md/core/form/Select";
import { Snackbar } from "@react-md/core/snackbar/Snackbar";
import { ToastManager } from "@react-md/core/snackbar/ToastManager";
import { ToastManagerProvider } from "@react-md/core/snackbar/ToastManagerProvider";
import { type SnackbarPosition } from "@react-md/core/snackbar/snackbarStyles";
import { type ReactElement, useState } from "react";
export default function SnackbarPositionExample(): ReactElement {
const [position, setPosition] = useState<SnackbarPosition>("bottom");
return (
<ToastManagerProvider manager={manager}>
<Button
onClick={() => {
manager.addToast({
children: "Hello, world!",
visibleTime: null,
});
}}
>
Toast!
</Button>
<Button
theme="warning"
themeType="outline"
onClick={() => {
manager.clearToasts();
}}
>
Remove all toasts
</Button>
<Select
label="Position"
value={position}
onChange={(event) => {
setPosition(event.currentTarget.value);
}}
>
{positions.map((position) => (
<Option key={position} value={position}>
{position}
</Option>
))}
</Select>
<Snackbar position={position} limit={3} />
</ToastManagerProvider>
);
}
const manager = new ToastManager();
const positions: readonly SnackbarPosition[] = [
"bottom",
"bottom-left",
"bottom-right",
"top",
"top-left",
"top-right",
];
Toast Priority
The default toast queue priority is first-in-first-out so that new toasts are always added to the end of the queue. However, there are cases where a new toast needs to be shown immediately to the user since some event happened that causes the application to no longer work. A few examples are losing internet connection or API requests failing.
To change the insert order for a new toast, include the priority
option which
can be set to one of the following:
"normal"
(default) - the toast will be added to the end of the queue"next"
- the toast will be inserted next-in-line in the queue, waiting for the current visible toast to exit before being shown. If the toast does not support duplicates, the existing toast will be moved instead and merged with the toast."replace"
- if there is a currently visible toast, it will start the leave transition and display the newly added toast instead."immediate"
- the same behavior as"replace"
except that if there was a currently visible toast, the toast will be shown again once the"immediate"
toast is hidden.
This example will try to show this behavior while logging the current toast queue.
Custom Toast Renderer
It is recommended to only look at this example for complex applications since it requires building most of the toast functionality manually and using some low-level components.
If the toasts in the application require additional business logic or a combination of complex components, it is recommended to create a custom toast renderer with an optional toast manager. Using both together is recommended since it is possible to enforce only known toasts by creating a custom toast manager and handle those specific toasts in the renderer.
This example will showcase how to create a toast system that:
- Enforces specific toast ids throughout the application
- Use specific themes, layouts, and timeouts based on the unique toast id
- Use the
DefaultToastRenderer
to implement rendering a toast will the normal visibility behavior - Add custom toast content with the
ToastContent
component - Add custom content in the toast based on specific
toastId
- Modify the toast visibility behavior with
useCurrentToastActions
oruseRemoveToast
hooks
"use client";
import { AsyncButton } from "@react-md/core/button/AsyncButton";
import { Button } from "@react-md/core/button/Button";
import { type BackgroundColor } from "@react-md/core/cssUtils";
import {
DefaultToastRenderer,
type ToastRendererProps,
} from "@react-md/core/snackbar/DefaultToastRenderer";
import { Snackbar } from "@react-md/core/snackbar/Snackbar";
import { ToastContent } from "@react-md/core/snackbar/ToastContent";
import { ToastManager } from "@react-md/core/snackbar/ToastManager";
import {
ToastManagerProvider,
useRemoveToast,
} from "@react-md/core/snackbar/ToastManagerProvider";
import { useCurrentToastActions } from "@react-md/core/snackbar/useCurrentToastActions";
import { wait } from "@react-md/core/utils/wait";
import { type ReactElement } from "react";
export default function CustomToastRendererExample(): ReactElement {
return (
<ToastManagerProvider manager={manager}>
<Button
onClick={() => {
addToast("Offline");
addToast("Success");
addToast("Undo");
addToast("Redo");
}}
>
Toast!
</Button>
<Snackbar renderToast={CustomToastRenderer} />
</ToastManagerProvider>
);
}
type ToastId = "Undo" | "Redo" | "Offline" | "Success";
const addToast = (toastId: ToastId): void => {
let theme: BackgroundColor = "surface";
let visibleTime: number | undefined | null;
let closeButton = true;
switch (toastId) {
case "Offline":
theme = "error";
// pretend like you have some logic to check online status
visibleTime = 10000;
closeButton = false;
break;
case "Undo":
theme = "warning";
break;
case "Success":
theme = "success";
break;
}
manager.addToast({
toastId,
theme,
visibleTime,
closeButton,
});
};
function assertKnownToast(_toastId: string): asserts _toastId is ToastId {
// pretend assertion
}
function AsyncAction({ toastId }: { toastId: ToastId }): ReactElement {
// If the current `toastId` is not available for some reason, use the
// `removeToast` returned from `useCurrentToastActions` instead.
const removeToast = useRemoveToast();
const { pauseRemoveTimeout } = useCurrentToastActions();
return (
<AsyncButton
onClick={async () => {
pauseRemoveTimeout();
// pretend some API call or business logic
await wait(3000);
// Use `false` if the toast exit transition should not occur when
// removing the toast
removeToast(toastId, true);
}}
>
{toastId}
</AsyncButton>
);
}
function CustomToastRenderer(props: ToastRendererProps): ReactElement {
const { toastId } = props;
assertKnownToast(toastId);
return (
<DefaultToastRenderer {...props} disableToastContent>
<ToastContent>{toastId}</ToastContent>
{toastId !== "Offline" && toastId !== "Success" && (
<AsyncAction toastId={toastId} />
)}
</DefaultToastRenderer>
);
}
const manager = new ToastManager();