Button w/ animated state changes

react #animation#ui
import { AnimatePresence, motion } from "framer-motion"

const buttonCopy = {
  idle: "Send me a login link",
  loading: <Spinner size={16} color="rgba(255, 255, 255, 0.65)" />,
  success: "Login link sent!",
}

function SmoothButton() {
  const [buttonState, setButtonState] = React.useState("idle")

  return (
    <div className="outer-wrapper">
      <button
        disabled={buttonState !== "idle"}
        onClick={() => {
          // This code is just a placeholder
          setButtonState("loading")

          setTimeout(() => {
            setButtonState("success")
          }, 1750)

          setTimeout(() => {
            setButtonState("idle")
          }, 3500)
        }}
      >
        {/* `wait` ensures the exiting elements finishes its animation before the entering element animates in. NOTE: this should be `popLayout` actually but it has a bug when used in shadow roots, which is how I isolate styles in my React demos: https://github.com/framer/motion/issues/2508 */}
        {/* `initial` prevents the animation from running on first render */}
        <AnimatePresence mode="wait" initial={false}>
          <motion.span
            key={buttonState}
            initial={{ opacity: 0, y: -25 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: 25 }}
            transition={{ type: "spring", duration: 0.25, bounce: 0 }}
          >
            {buttonCopy[buttonState]}
          </motion.span>
        </AnimatePresence>
      </button>
    </div>
  )
}

function Spinner({ color, size = 20 }) {
  const bars = Array(12).fill(0)

  return (
    <div
      className="wrapper"
      style={{
        ["--spinner-size"]: `${size}px`,
        ["--spinner-color"]: color,
      }}
    >
      <div className="spinner">
        {bars.map((_, i) => (
          <div className="bar" key={`spinner-bar-${i}`} />
        ))}
      </div>
    </div>
  )
}

const styles = (
  <style>{`
button {
  border-radius: 8px;
  font-weight: 500;
  font-size: 13px;
  height: 32px;
  min-width: 148px;
  overflow: hidden;
  background: linear-gradient(180deg, #1994ff 0%, #157cff 100%);
  box-shadow:
    0px 0px 1px 1px rgba(255, 255, 255, 0.08) inset,
    0px 1px 1.5px 0px rgba(0, 0, 0, 0.32),
    0px 0px 0px 0.5px #1a94ff;
  position: relative;
}

button span {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: center;
  color: white;
  text-shadow: 0px 1px 1.5px rgba(0, 0, 0, 0.16);
}

.outer-wrapper {
  display: flex;
  padding: 120px 40px;
  justify-content: center;
}

.wrapper {
  height: var(--spinner-size, 20px);
  width: var(--spinner-size, 20px);
}

.spinner {
  position: relative;
  top: 50%;
  left: 50%;
  height: var(--spinner-size, 20px);
  width: var(--spinner-size, 20px);
}

.bar {
  animation: spin 1.2s linear infinite;
  background: var(--spinner-color);
  border-radius: 6px;
  height: 8%;
  left: -10%;
  position: absolute;
  top: -3.9%;
  width: 24%;
}

.bar:nth-child(1) {
  animation-delay: -1.2s;
  transform: rotate(0.0001deg) translate(146%);
}

.bar:nth-child(2) {
  animation-delay: -1.1s;
  transform: rotate(30deg) translate(146%);
}

.bar:nth-child(3) {
  animation-delay: -1s;
  transform: rotate(60deg) translate(146%);
}

.bar:nth-child(4) {
  animation-delay: -0.9s;
  transform: rotate(90deg) translate(146%);
}

.bar:nth-child(5) {
  animation-delay: -0.8s;
  transform: rotate(120deg) translate(146%);
}

.bar:nth-child(6) {
  animation-delay: -0.7s;
  transform: rotate(150deg) translate(146%);
}

.bar:nth-child(7) {
  animation-delay: -0.6s;
  transform: rotate(180deg) translate(146%);
}

.bar:nth-child(8) {
  animation-delay: -0.5s;
  transform: rotate(210deg) translate(146%);
}

.bar:nth-child(9) {
  animation-delay: -0.4s;
  transform: rotate(240deg) translate(146%);
}

.bar:nth-child(10) {
  animation-delay: -0.3s;
  transform: rotate(270deg) translate(146%);
}

.bar:nth-child(11) {
  animation-delay: -0.2s;
  transform: rotate(300deg) translate(146%);
}

.bar:nth-child(12) {
  animation-delay: -0.1s;
  transform: rotate(330deg) translate(146%);
}

@keyframes spin {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0.15;
  }
}
`}</style>
)

export default function Demo() {
  return (
    <>
      {styles}
      <SmoothButton />
    </>
  )
}