Stepper Button

An interactive stepper component with functional plus/minus controls and smooth vertical swipe animations for the numeric count.

React Code

"use client";

import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

function MinusIcon({ className }: { className?: string }) {
  return (
    <svg className={className} preserveAspectRatio="none" overflow="visible" style={{ display: "block" }} width="18" height="2" viewBox="0 0 17.5 1.5" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M0.75 0.75H16.75" stroke="#2D264B" strokeWidth="1.5" strokeLinecap="round"/>
    </svg>
  );
}

function AddIcon({ className }: { className?: string }) {
  return (
    <svg className={className} preserveAspectRatio="none" overflow="visible" style={{ display: "block" }} width="18" height="18" viewBox="0 0 17.5 17.5" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M0.75 8.75H16.75M8.75 16.75V8.75L8.75 0.75" stroke="#2D264B" strokeWidth="1.5" strokeLinecap="round"/>
    </svg>
  );
}

export function StepperButton() {
  const [count, setCount] = useState(0);
  const [direction, setDirection] = useState(1);

  const increment = () => {
    setDirection(1);
    setCount((prev) => prev + 1);
  };

  const decrement = () => {
    setDirection(-1);
    setCount((prev) => (prev > 0 ? prev - 1 : 0));
  };

  return (
    <div className="bg-white border border-[#d8dee9] border-solid flex items-center justify-between overflow-hidden relative rounded-[12px] h-[52px] min-w-[140px]">
      
      {/* Minus Button */}
      <motion.button
        type="button"
        onClick={decrement}
        whileHover={{ backgroundColor: "#d1d6dc" }}
        whileTap={{ scale: 0.95 }}
        className="bg-[#e6e9ec] flex h-full items-center justify-center overflow-hidden px-[16px] py-[14px] relative shrink-0 outline-none cursor-pointer border-none"
      >
        <div className="flex items-center justify-center w-[24px] h-[24px]">
          <MinusIcon />
        </div>
      </motion.button>
      
      {/* Center Number with Swiping Animation */}
      <div className="flex-1 flex items-center justify-center relative overflow-hidden h-full bg-white min-w-[40px]">
        <AnimatePresence mode="popLayout" initial={false} custom={direction}>
          <motion.p
            key={count}
            custom={direction}
            initial={{ y: direction > 0 ? "100%" : "-100%", opacity: 0 }}
            animate={{ y: "0%", opacity: 1 }}
            exit={{ y: direction > 0 ? "-100%" : "100%", opacity: 0 }}
            transition={{ type: "spring", stiffness: 400, damping: 30 }}
            className="absolute font-['Plus_Jakarta_Sans',sans-serif] font-normal leading-[normal] text-[#1f2123] text-[14px] whitespace-nowrap m-0 select-none"
          >
            {count}
          </motion.p>
        </AnimatePresence>
      </div>
      
      {/* Plus Button */}
      <motion.button
        type="button"
        onClick={increment}
        whileHover={{ backgroundColor: "#d1d6dc" }}
        whileTap={{ scale: 0.95 }}
        className="bg-[#e6e9ec] flex h-full items-center justify-center overflow-hidden px-[16px] py-[14px] relative shrink-0 outline-none cursor-pointer border-none"
      >
        <div className="flex items-center justify-center w-[24px] h-[24px]">
          <AddIcon />
        </div>
      </motion.button>
      
    </div>
  );
}