Creating A Speed Dial React Component

07/09/2022

Intro

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.

Demo

Before reading the article, it might be nice to use the finished SpeedDial to see how it works.

Planning Our Components

Our SpeedDial will consist of two components:

  • SpeedDial
  • SpeedDialAction

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.

SpeedDial

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.

SpeedDial.css

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:

Icon Component

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.

SpeedDialAction

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.

SpeedDialAction.css

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:

Implementation

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.

Extras

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.

Rotating The Plus Icon

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!

Setting An Animation On The SpeedDialAction Components

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.

Summary

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.

Want To Level Up Your JavaScript Game?

Book a private session with me, and you will be writing slick JavaScript code in no time.