This is a tutorial to program your own Snackbar with React. The end result will be like the one that is triggered with the “Show snackbar” button below.
Startup
You can find the source code here. The startup
branch contains the initial setup files. You can also get it directly from the command line.
$ git clone -b startup git@github.com:fabrizzio-gz/simple-react-snackbar.git $ cd simple-react-snackbar $ npm install
The final folder structure of the project will look as follows:
. ├── public │ └── index.html └── src ├── App.js ├── components │ ├── MessageInput │ │ ├── MessageInput.css │ │ └── MessageInput.jsx │ ├── Snackbar │ │ ├── Snackbar.css │ │ └── Snackbar.jsx │ └── store │ └── snackbar-context.js └── index.js
Initially, our parent App
component renders only the MessageInput
component. We will use it only to trigger the snackbar with a certain message. The actual Snackbar is independent of this component.
// App.js import React from "react"; import MessageInput from "./components/MessageInput"; const App = () => <MessageInput />; export default App;
// MessageInput.jsx import React from "react"; import "./MessageInput.css"; const MessageInput = () => { return ( <div className="app__container"> <div className="app__center"> <label htmlFor="snackbar-msg">Message</label> <input id="snackbar-msg" type="text" /> </div> <div className="app__center"> <button className="app__button">Show snackbar</button> </div> </div> ); }; export default MessageInput;
At this state, our App
should render as shown below.
Snackbar markup
Let’s proceed now to create the Snackbar
component. At its core, it’s just a div
with a label and a button to close it.
// Snackbar.jsx import React from "react"; import "./Snackbar.css"; const Snackbar = () => { return ( <div className="snackbar__container"> <div className="snackbar__label">Hello!</div> <div className="snackbar__dismiss">×</div> </div> ); }; export default Snackbar;
The corresponding styles are present in the Snackbar.css
file. They were copied from the Material Design snackbar demo and are optimized only for large displays. Optimization for mobile devices would require some additional CSS rules that you can add if needed.
The snackbar would be rendered as shown below (although with a smaller width).
Adding the snackbar context
The state of the snackbar will be managed with the Context API. We do so so that we can trigger the snackbar from anywhere inside the App without having to pass props between components.
We start by creating a new file snackbar-context.js
. It contains the context object with the snackbar state and state handlers:
// snackbar-context.js import React, { useState } from "react"; const SnackbarContext = React.createContext({ isDisplayed: false, displayMsg: (msg) => {}, onClose: () => {}, }); export default SnackbarContext;
msg
will contain the snackbar text to be shown. isDisplayed
will control whether the snackbar is displayed or not. displayMsg
will trigger the snackbar with a certain message. onClose
will be called from the dismiss button of the snackbar to close it. Latere on we will add a timer so that if the snackbar isn’t closed by the user, it will be closed automatically after a certain time.
Additionally, snackbar-context.js
contains a custom context provider component that will wrap the whole App insideSnackbarContext.Provider
. This wrapper is necessary to be able to consume the context from within child components.
This custom component will also manage the state variables of the snackbar: msg
and isDisplayed
. The display and close handlers modify those state variables for the snackbar to function as we want.
// snackbar-context.js export const SnackBarContextProvider = (props) => { const [msg, setMsg] = useState(""); const [isDisplayed, setIsDisplayed] = useState(false); const displayHandler = (msg) => { setMsg(msg); setIsDisplayed(true); timer = setTimeout(() => { closeHandler(); }, 3000); // close snackbar after 3 seconds }; const closeHandler = () => { clearTimeout(timer); setIsDisplayed(false); }; return ( <SnackbarContext.Provider value={{ msg, isDisplayed, displayMsg: displayHandler, onClose: closeHandler, }} > {props.children} </SnackbarContext.Provider> ); };
As it was said, the whole App
will be wrapped in this custom context provider component. We do so with the following modifications in index.js
.
//index.js import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import { SnackBarContextProvider } from "./components/store/snackbar-context"; ReactDOM.render( <SnackBarContextProvider> <App /> </SnackBarContextProvider>, document.getElementById("root") );
Adding the snackbar to the App
The Snackbar
is rendered from App dynamically based on the context isDisplayed
state.
//App.js import React, { useContext } from "react"; import MessageInput from "./components/MessageInput"; import Snackbar from "./components/Snackbar"; import SnackbarContext from "./components/store/snackbar-context"; const App = () => { const snackbarCtx = useContext(SnackbarContext); return ( <> <MessageInput /> {snackbarCtx.isDisplayed && <Snackbar />} </> ); }; export default App;
See that useContext
allows us to import the context.
Displaying a dynamic message
So far, our snackbar displays the static text Hello!
. Instead, we would like it to show a dynamic text. We can do so by making it display the msg
state of SnackbarContext
.
// Snackbar.jsx import React, { useContext } from "react"; import "./Snackbar.css"; import SnackbarContext from "./store/snackbar-context"; const Snackbar = () => { const snackbarCtx = useContext(SnackbarContext); return ( <div className="snackbar__container"> <div className="snackbar__label">{snackbarCtx.msg}</div> <div className="snackbar__dismiss" onClick={snackbarCtx.onClose}> × </div> </div> ); }; export default Snackbar;
Additionally, we have added the onClose
handler to the dismiss button of the snackbar. This way the user is able to close the snackbar by pressing on it. See that this handler also comes from the SnackbarContext
.
Once again, we consume the context state by calling useContext
.
Triggering the snackbar
At this point, our snackbar, although quite basic, is functional. We can trigger it from anywhere inside the app. Let’s see how to do so from the MessageInput
component.
// MessageInput.jsx import React, { useContext, useRef } from "react"; import "./MessageInput.css"; import SnackbarContext from "./store/snackbar-context"; const MessageInput = () => { const inputRef = useRef(); const snackbarCtx = useContext(SnackbarContext); const clickHandler = () => { const msg = inputRef.current.value; snackbarCtx.displayMsg(msg); }; return ( <div className="app__container"> <div className="app__center"> <label htmlFor="snackbar-msg">Message</label> <input ref={inputRef} id="snackbar-msg" type="text" /> </div> <div className="app__center"> <button className="app__button" onClick={clickHandler}> Show snackbar </button> </div> </div> ); }; export default MessageInput;
The basic process is to call SnackbarContext.displayMsg
with the message we would like to display. The useRef hook was used to access the contents of the input
element. We call displayMsg
with that value.
A similar method would be used in any other app to trigger the snackbar.
Animating the snackbar
Finally, we’ll add a couple of extra features. First, we’ll animate the snackbar every time it’s displayed.
We can do so by setting the animate
CSS property of the snackbar__container
class. The animation is defined as follows:
/* Snackbar.css */ @keyframes slide-up { from { opacity: 0; transform: translateY(3rem); } to { opacity: 1; transform: translateY(0); } }
The corresponding CSS rule:
/* Snackbar.css */ .snackbar__container { ... animation: 300ms ease-out forwards slide-up; }
With these additions, the snackbar will be animated every time it’s displayed.
Rendering the snackbar inside its own container
Our Snackbar component is rendered as a child of the App
components inside the root
div element. While this is ok for this example, larger projects could need rendering the snackbar inside a different HTML element. The reason could be to improve the semantics of the application or to avoid rendering the snackbar inside a deeply nested component.
To render the snackbar at a different place, without modifying the strructure of our React components, we can use createPortal.
We will start by adding a new div
element to index.html
.
<!-- index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>React Snackbar</title> </head> <body> <div id="root"></div> <div id="snackbar__root"></div> </body> </html>
See the id
attribute is set to snackbar__root
. The name is not important but we need it to identify it.
Next, we’ll modify Snackbar
by asking React to render it at a different DOM node. This is done with createPortal
, which is imported from react-dom
.
// Snackbar.jsx import React, { useContext } from "react"; import ReactDOM from "react-dom"; import "./Snackbar.css"; import SnackbarContext from "./store/snackbar-context"; const Snackbar = () => { const snackbarCtx = useContext(SnackbarContext); return ReactDOM.createPortal( <div className="snackbar__container"> <div className="snackbar__label">{snackbarCtx.msg}</div> <div className="snackbar__dismiss" onClick={snackbarCtx.onClose}> × </div> </div>, document.getElementById("snackbar__root") ); }; export default Snackbar;
With these changes, the snackbar HTML elements will be rendered inside the snackbar__root
div element.
This concludes this tutorial to create a basic Snackbar with React. You can access the complete source code here.