Today I am going to cover how to create a SpeedDial navigation component with React. It is inspired by Material UI's implementation, which you can mess around with here. Before we get started, let's cover what a SpeedDial component actually does.
Basically, whenever the main button is hovered, some additional items will appear. For sake of article length, I will only be showing how to display these additional items to the right of the main button. If you checked out the Material UI version, you would have seen that it can display the items to the left, top, right, or bottom of the main button.
The demo below will show you everything you need to know.
Before reading the article, it might be nice to use the finished SpeedDial to see how it works.
Our SpeedDial will consist of two components:
SpeedDial
will contain a main button, but will also serve as a container for the SpeedDialAction
components, where
they will be rendered as children. The JSX diagram below shows this.
Let's get the beginning phases of SpeedDial
out of the way. To do so, let's create a file called SpeedDial.js
with
the following code.
import React, { useState } from "react";
import Icon from "./Icon";
import "./SpeedDial.css";
export default function SpeedDial({ children }) {
const [hovered, setHovered] = useState(false);
return (
<div
className="speed-dial"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => {
setHovered(false);
}}
onClick={() => {
setHovered(!hovered);
}}
>
<button className="main-btn">
<Icon
name="plus"
/>
</button>
<div className="action-wrapper">{hovered && children}</div>
</div>
);
}
What this does is create a container with a button, and another inside container for children to be displayed. The
hovered
piece of state controls whether the children (SpeedDialAction components) will be visible. This will
become more clear in a minute when we create an App.js
file and actually implement the SpeedDial.
Here is the CSS for SpeedDial
.
.speed-dial {
display: inline-flex;
position: relative;
}
.speed-dial > .main-btn {
background: rgb(25, 118, 210); /* blue */
height: 60px;
width: 60px;
border-radius: 50%;
color: white;
font-size: 18px;
cursor: pointer;
box-shadow: rgb(0 0 0 / 20%) 0px 3px 5px -1px,
rgb(0 0 0 / 14%) 0px 6px 10px 0px, rgb(0 0 0 / 12%) 0px 1px 18px 0px;
}
.speed-dial > .action-wrapper {
display: flex;
align-items: center;
}
With that in place, the SpeedDial
component should look like this:
Since I am using Font Awesome, I decided to create a reusable Icon
component, to cut down on typing.
import React from "react";
export default function Icon({ name, style }) {
return <i className={`fa fa-${name}`} style={style} />;
}
Nothing crazy going on here, our component accepts a prop for its Font Awesome name, and a style object just in case
we want to apply custom styles to it. This should explain the use of Icon
in the SpeedDial.js
file.
Let's create a new file SpeedDialAction.js
and put in the following code:
import React from 'react';
import Icon from './Icon';
import './SpeedDialAction.css';
export default function SpeedDialAction({
icon,
label,
hovered,
setActiveAction,
onClick,
}) {
return (
<div className="speed-dial-action" onClick={onClick}>
{hovered && <div className="label">{label}</div>}
<button
onMouseEnter={() => setActiveAction(label)}
onMouseLeave={() => setActiveAction('')}
>
<Icon name={icon} />
</button>
</div>
);
}
These props will make more sense later on, don't worry too much about them for now.
Here is the CSS for SpeedDialAction
.
.speed-dial-action {
display: flex;
flex-direction: column;
position: relative;
}
.speed-dial-action button {
box-shadow: rgb(0 0 0 / 20%) 0px 3px 5px -1px,
rgb(0 0 0 / 14%) 0px 6px 10px 0px, rgb(0 0 0 / 12%) 0px 1px 18px 0px;
background: white;
height: 45px;
width: 45px;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
}
.speed-dial-action button:hover {
background: #d8d8d8;
}
.speed-dial-action button {
margin-left: 15px;
}
.speed-dial-action .label {
background: #565656;
color: white;
padding: 5px 8px;
position: absolute;
border-radius: 4px;
text-transform: capitalize;
}
.speed-dial-action .label {
top: -40px;
transform: translateX(8px);
}
With the CSS in place, a SpeedDialAction component should look something like this:
Up to now, we are just kind of assuming everything works and is styled correctly. Let's actually implement
our SpeedDial in a file called App.js
.
import { useState } from 'react';
import SpeedDial from './components/SpeedDial';
import SpeedDialAction from './components/SpeedDialAction';
import './styles.css';
const actions = [
{ label: 'share', icon: 'share ' },
{ label: 'print', icon: 'print' },
{ label: 'save', icon: 'floppy-o' },
{ label: 'copy', icon: 'copy' },
];
export default function App() {
const [activeAction, setActiveAction] = useState('');
return (
<div className="App">
<SpeedDial>
{actions.map((action) => (
<SpeedDialAction
key={action.label}
setActiveAction={setActiveAction}
hovered={action.label === activeAction}
label={action.label}
icon={action.icon}
onClick={() => alert(`You selected ${action.label}`)}
/>
))}
</SpeedDial>
</div>
);
}
This should result in the following when we hover the main blue button now:
Hopefully the props passed to SpeedDialAction
make more sense now.
Currently there are two things wrong:
The plus icon inside of the main button doesn't turn to an x when the SpeedDial
is hovered. Pressing this x should
hide the SpeedDialAction
components.
There is no animation when displaying the SpeedDialAction
components, it is somewhat abrupt.
Let's tackle these problems now.
This is very easy to do, and will result in an x icon. All we need to do is set a conditional style on the Icon
inside of SpeedDial.js
.
<button className="main-btn">
<Icon
name="plus"
style={{
transform: hovered ? 'rotate(45deg)' : 'none',
transition: 'transform ease .25s',
}}
/>
</button>
When hovered, all we do is rotate the plus sign 45 degrees, and we have our close icon!
For this one, we are going to make use of CSS animations, and use opacity to make things appear more smoothly.
Let's add some code to our SpeedDialAction.css
file.
@keyframes load {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.speed-dial-action {
display: flex;
flex-direction: column;
position: relative;
animation-name: load;
animation-duration: 0.25s;
}
And that's it! Our SpeedDialAction
components should now appear more smoothly when the main button is hovered because
setting the opacity from 0 to 1 is slightly delayed by .25s
.
That's about it for our SpeedDial React Component folks. As always, if you have any problems understanding any of this, feel free to shoot me a message, and I'll make sure it's crystal clear.