1. Carrito
  2. Envío
  3. Pago
  4. Listo

Tu carrito

Auriculares Aura + funda2 artículos
React + MotionNavigation

Checkout-Stepper

Bestellschritte mit elastischer Fortschrittslinie, Häkchen, die beim Abschluss aufspringen, und Inhalten, die in die richtige Richtung gleiten.

Einstellungen

Code

import { useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { Check } from "lucide-react";

const CHECKOUT_STEPS = [
  {
    label: "Warenkorb",
    title: "Dein Warenkorb",
    body: "Aura Kopfhörer + Hülle",
    meta: "2 Artikel",
  },
  { label: "Versand", title: "Lieferung", body: "Calle Mayor 14, Madrid", meta: "Donnerstag, 12." },
  { label: "Zahlung", title: "Zahlung", body: "Visa mit Endziffern 4242", meta: "Keine Gebühren" },
  {
    label: "Fertig",
    title: "Bestellung bestätigt",
    body: "Beleg per E-Mail gesendet",
    meta: "#A-20931",
  },
];

interface CheckoutStepperProps {
  color?: string;
  showLabels?: boolean;
}

export default function CheckoutStepper({
  color = "#635bff",
  showLabels = true,
}: CheckoutStepperProps) {
  const [step, setStep] = useState(0);
  const [dir, setDir] = useState(1);
  const reduce = useReducedMotion();
  const last = CHECKOUT_STEPS.length - 1;
  const current = CHECKOUT_STEPS[step]!;
  const done = step === last;

  const go = (i: number) => {
    setDir(i >= step ? 1 : -1);
    setStep(i);
  };

  const primary = step === 2 ? "48,90 € bezahlen" : done ? "Neue Bestellung" : "Weiter";
  const slide = reduce ? 0 : 28;

  return (
    <div className="w-[22rem] max-w-full rounded-[22px] bg-white p-5 text-[#1d1d1f] shadow-[0_1px_2px_rgba(0,0,0,0.04),0_12px_40px_rgba(0,0,0,0.08)] ring-1 ring-black/[0.05] dark:bg-[#1c1c1e] dark:text-[#f5f5f7] dark:shadow-[0_12px_40px_rgba(0,0,0,0.45)] dark:ring-white/10">
      <div className="relative">
        <div
          aria-hidden
          className="absolute left-3 right-3 top-3 h-[2px] -translate-y-1/2 rounded-full bg-black/[0.06] dark:bg-white/10"
        >
          <motion.div
            className="h-full rounded-full"
            style={{ backgroundColor: color }}
            initial={false}
            animate={{ width: (step / last) * 100 + "%" }}
            transition={{ type: "spring", stiffness: 140, damping: 22 }}
          />
        </div>
        <ol className="relative flex justify-between">
          {CHECKOUT_STEPS.map((s, i) => {
            const complete = i < step || done;
            const isCurrent = i === step && !done;
            return (
              <li key={s.label} className="relative flex w-6 flex-col items-center">
                <button
                  type="button"
                  disabled={i >= step || done}
                  onClick={() => go(i)}
                  aria-label={s.label + (complete ? " (hecho)" : "")}
                  aria-current={i === step ? "step" : undefined}
                  className="relative grid h-6 w-6 place-items-center rounded-full bg-white text-[11px] font-semibold tabular-nums outline-none transition-[box-shadow,background-color,color] duration-300 focus-visible:ring-2 focus-visible:ring-offset-2 enabled:hover:scale-110 disabled:cursor-default dark:bg-[#1c1c1e] dark:focus-visible:ring-offset-[#1c1c1e]"
                  style={{
                    backgroundColor: complete ? color : undefined,
                    color: complete ? "#fff" : isCurrent ? color : "#aeaeb2",
                    boxShadow: complete
                      ? "none"
                      : isCurrent
                        ? "inset 0 0 0 2px " + color + ", 0 0 0 5px " + color + "22"
                        : "inset 0 0 0 1.5px rgba(128,128,128,0.3)",
                    ["--tw-ring-color" as string]: color,
                  }}
                >
                  <AnimatePresence mode="popLayout" initial={false}>
                    {complete ? (
                      <motion.span
                        key="check"
                        initial={{ scale: 0, rotate: -45 }}
                        animate={{ scale: 1, rotate: 0 }}
                        transition={{ type: "spring", stiffness: 520, damping: 22 }}
                        className="grid place-items-center"
                      >
                        <Check className="h-3.5 w-3.5" strokeWidth={3} />
                      </motion.span>
                    ) : (
                      <motion.span
                        key="n"
                        initial={{ opacity: 0 }}
                        animate={{ opacity: 1 }}
                        exit={{ opacity: 0 }}
                      >
                        {i + 1}
                      </motion.span>
                    )}
                  </AnimatePresence>
                </button>
                {showLabels && (
                  <span
                    className="absolute top-8 whitespace-nowrap text-[11px] font-medium transition-colors duration-300"
                    style={{ color: i <= step ? undefined : "#aeaeb2" }}
                  >
                    {s.label}
                  </span>
                )}
              </li>
            );
          })}
        </ol>
      </div>

      <div className={"relative overflow-hidden " + (showLabels ? "mt-11" : "mt-6")}>
        <AnimatePresence mode="popLayout" custom={dir} initial={false}>
          <motion.div
            key={step}
            custom={dir}
            initial={{ x: dir * slide, opacity: 0, filter: "blur(4px)" }}
            animate={{ x: 0, opacity: 1, filter: "blur(0px)" }}
            exit={{ x: dir * -slide, opacity: 0, filter: "blur(4px)" }}
            transition={{ type: "spring", stiffness: 320, damping: 32 }}
          >
            <p className="text-[20px] font-semibold tracking-[-0.02em]">{current.title}</p>
            <div className="mt-3 flex items-center justify-between gap-3 border-t border-black/[0.06] pt-3 text-[13px] dark:border-white/10">
              <span className="truncate text-[#86868b]">{current.body}</span>
              <span className="shrink-0 font-medium tabular-nums">{current.meta}</span>
            </div>
          </motion.div>
        </AnimatePresence>
      </div>

      <div className="mt-5 flex items-center gap-2">
        <AnimatePresence initial={false}>
          {step > 0 && !done && (
            <motion.button
              type="button"
              onClick={() => go(step - 1)}
              initial={{ opacity: 0, x: -6 }}
              animate={{ opacity: 1, x: 0 }}
              exit={{ opacity: 0, x: -6 }}
              whileTap={{ scale: 0.96 }}
              className="h-10 rounded-full px-4 text-[13px] font-medium text-[#86868b] outline-none transition-colors hover:bg-black/[0.05] hover:text-[#1d1d1f] focus-visible:ring-2 focus-visible:ring-black/20 dark:hover:bg-white/10 dark:hover:text-[#f5f5f7] dark:focus-visible:ring-white/30"
            >
              Zurück
            </motion.button>
          )}
        </AnimatePresence>
        <motion.button
          type="button"
          layout
          whileHover={{ y: -1 }}
          whileTap={{ scale: 0.97, y: 0 }}
          transition={{ type: "spring", stiffness: 500, damping: 30 }}
          onClick={() => go(done ? 0 : step + 1)}
          className="ml-auto h-10 rounded-full px-5 text-[13px] font-semibold text-white outline-none focus-visible:ring-2 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-[#1c1c1e]"
          style={{
            backgroundColor: color,
            boxShadow:
              "inset 0 1px 0 rgba(255,255,255,0.2), 0 1px 2px rgba(0,0,0,0.12), 0 8px 20px " +
              color +
              "40",
            ["--tw-ring-color" as string]: color,
          }}
        >
          {primary}
        </motion.button>
      </div>
    </div>
  );
}

Prompt für KI

Füg ihn in ChatGPT, Claude oder Cursor ein und der Baustein wird mit diesen Einstellungen an dein Projekt angepasst, auch ohne React.

Mehr aus Navigation

Wofür du ihn nutzen kannst

Beim Bezahlen springen im Onlineshop die meisten ab, und zu wissen, wie viel noch fehlt, hilft. Dieser Checkout-Stepper führt vom Warenkorb über Versand und Zahlung bis zur Bestätigung, und der Hauptbutton wird im passenden Schritt zu „48,90 € bezahlen“. Er eignet sich auch für jedes mehrstufige Formular, etwa eine Buchung.

Bleib bei drei bis fünf Schritten, sonst wird es eng. Erledigte Schritte lassen sich anklicken, um zurückzugehen, kommende nicht, so entstehen keine halb ausgefüllten Bestellungen. Ist die Karte schmal, blende die Beschriftungen aus und lass die Zahlen sprechen.