How To Animate Buttons In A Speed Dial

Viewed 43

I am building my own SpeedDial which is inspired by the Material UI one, but can't currently figure out how I can make the hidden buttons transition smoothly like they do in the example link above when the SpeedDial is hovered. It's not like they are sliding out, but each button appears one after the other smoothly. If you go to the link above, you will see what I mean. Select the "left" direction, since it is most like my example code.

Here is a quick screenshot of what the SpeedDial looks like, just in case you are wondering.

enter image description here

I have a working CodeSandbox where I have the entire Speed Dial built, but it's lacking the animation I am looking for. My initial thought was to not conditionally render the components, and have them always visible, but use visibility: hidden. However, I don't think doing that solves my problem, and won't allow for any transitions to be set.

For sake of completeness, I will include all of the code, but I highly recommend that you just mess around with the CodeSandbox, as it already works.

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
        direction="left"
        style={{
          position: "absolute",
          bottom: 40,
          right: 40
        }}
      >
        {actions.map((action) => (
          <SpeedDialAction
            key={action.label}
            direction="left"
            setActiveAction={setActiveAction}
            hovered={action.label === activeAction}
            label={action.label}
            icon={action.icon}
          />
        ))}
      </SpeedDial>
    </div>
  );
}

Basically, the SpeedDial component accepts children, specifically an array that will create several SpeedDialAction components. When the SpeedDial button is hovered, the SpeedDialAction components become visible next to the main SpeedDial button (the blue button). When the mouse is moved out of the SpeedDial, they disappear.

SpeedDial.js

import React, { useState } from "react";
import Icon from "./Icon";

import "./SpeedDial.css";

export default function SpeedDial({ children, direction, style }) {
  const [hovered, setHovered] = useState(false);

  return (
    <div
      className={`speed-dial ${direction}`}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => {
        setHovered(false);
      }}
      onClick={() => {
        setHovered(!hovered);
      }}
      style={style}
    >
      <button className="main-btn">
        <Icon
          name="plus"
          style={{
            transform: hovered ? "rotate(45deg" : "none",
            transition: "transform ease .25s"
          }}
        />
      </button>

      <div className={`action-wrapper ${direction}`}>{hovered && children}</div>
    </div>
  );
}

SpeedDial.defaultProps = {
  direction: "right",
  onClick: () => {}
};

Notice how in the action-wrapper that this is where we are conditionally rendering the SpeedDialAction components via the children prop. I am aware that conditionally rendering like this works pretty much like display: none, and won't allow me to animate it. That's why I hope you can help!

SpeedDial.css

.speed-dial {
  display: inline-flex;
  position: relative;
  align-items: center;
  width: auto;
}

.speed-dial.top {
  flex-direction: column-reverse;
  align-items: flex-start;
  justify-content: center;
}

.speed-dial.bottom {
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
}

.speed-dial.left {
  flex-direction: row-reverse;
}

.speed-dial > .main-btn {
  background: rgb(25, 118, 210);
  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;
}

.speed-dial > .action-wrapper.top {
  flex-direction: column;
  align-items: flex-start;
  transform: translateX(7px);
}

.speed-dial > .action-wrapper.bottom {
  flex-direction: column;
  align-items: flex-start;
  transform: translateX(7px);
}

SpeedDialAction.js

import React from "react";
import Icon from "./Icon";

import "./SpeedDialAction.css";

export default function SpeedDialAction({
  icon,
  label,
  hovered,
  setActiveAction,
  direction,
  onClick
}) {
  return (
    <div className="speed-dial-action" onClick={onClick}>
      {hovered && <div className={`label ${direction}`}>{label}</div>}

      <button
        onMouseEnter={() => setActiveAction(label)}
        onMouseLeave={() => setActiveAction("")}
        className={direction}
      >
        <Icon name={icon} />
      </button>
    </div>
  );
}

Nothing crazy going on here, I just added it for sake of thoroughness.

SpeedDialAction.css

.speed-dial-action {
  display: flex;
  flex-direction: column;
  align-items: center;
  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.left {
  margin-right: 15px;
}

.speed-dial-action button.right {
  margin-left: 15px;
}

.speed-dial-action button.bottom {
  margin-top: 15px;
}

.speed-dial-action button.top {
  margin-bottom: 15px;
}

.speed-dial-action .label {
  background: #565656;
  color: white;
  padding: 5px 8px;
  position: absolute;
  border-radius: 4px;
  text-transform: capitalize;
}

.speed-dial-action .label.left {
  top: -40px;
}

.speed-dial-action .label.right {
  top: -40px;
}
0 Answers
Related