React + MotionMikrointeraktionen

Merken-Button mit Lesezeichen

Das Lesezeichen füllt sich von oben nach unten, sein Band dehnt sich wie Gummi und ein kurzes „Gespeichert“ taucht auf.

Einstellungen

Code

import { useEffect, useId, useRef, useState } from "react";
import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useTransform } from "framer-motion";

interface BookmarkSaveProps {
  color?: string;
  savedLabel?: string;
}

export default function BookmarkSave({
  color = "#ffb800",
  savedLabel = "Gespeichert",
}: BookmarkSaveProps) {
  const [saved, setSaved] = useState(false);
  const [chip, setChip] = useState(false);
  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  const clipId = "bm" + useId().replace(/[^a-zA-Z0-9]/g, "");
  const pull = useMotionValue(0);
  const reduce = useReducedMotion();
  const ribbon = useTransform(pull, (p) => {
    const tail = 21 + p * 3;
    const notch = 16.5 + p * 1.5;
    return "M5 5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2V" + tail + "L12 " + notch + "L5 " + tail + "Z";
  });

  useEffect(() => () => clearTimeout(timer.current), []);

  const toggle = () => {
    const next = !saved;
    setSaved(next);
    setChip(next);
    clearTimeout(timer.current);
    if (next) timer.current = setTimeout(() => setChip(false), 1400);
    if (reduce) return;
    animate(pull, 0, { type: "spring", velocity: next ? 11 : -6, stiffness: 420, damping: 9 });
  };

  return (
    <div className="relative">
      <AnimatePresence>
        {chip && (
          <motion.span
            className="pointer-events-none absolute bottom-full left-1/2 mb-2 -translate-x-1/2 whitespace-nowrap"
            initial={{ opacity: 0, y: 6, filter: "blur(4px)" }}
            animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
            exit={{ opacity: 0, y: -4, filter: "blur(4px)" }}
            transition={{ type: "spring", stiffness: 500, damping: 30 }}
          >
            <span className="block rounded-full bg-[#1d1d1f] px-2.5 py-1 text-xs font-medium text-white shadow-[0_4px_12px_rgba(0,0,0,0.15)] dark:bg-white dark:text-[#1d1d1f]">
              {savedLabel}
            </span>
          </motion.span>
        )}
      </AnimatePresence>
      <motion.button
        type="button"
        onClick={toggle}
        aria-pressed={saved}
        aria-label={saved ? "Aus Gespeichertem entfernen" : "Speichern"}
        whileTap={{ scale: 0.86 }}
        transition={{ type: "spring", stiffness: 600, damping: 24 }}
        className="grid h-14 w-14 place-items-center rounded-full text-[#1d1d1f] outline-none transition-colors hover:bg-black/[0.04] focus-visible:ring-2 focus-visible:ring-black/20 dark:text-[#f5f5f7] dark:hover:bg-white/[0.06] dark:focus-visible:ring-white/30"
      >
        <svg viewBox="0 0 24 24" className="h-8 w-8 overflow-visible" aria-hidden="true">
          <defs>
            <clipPath id={clipId}>
              <motion.path d={ribbon} />
            </clipPath>
          </defs>
          <g clipPath={"url(#" + clipId + ")"}>
            <motion.rect
              x="0"
              width="24"
              height="28"
              fill={color}
              initial={false}
              animate={{ y: saved ? -1 : -29 }}
              transition={saved ? { type: "spring", stiffness: 160, damping: 18 } : { duration: 0.22, ease: [0.4, 0, 1, 1] }}
            />
          </g>
          <motion.path
            d={ribbon}
            fill="none"
            strokeWidth={1.5}
            strokeLinejoin="round"
            initial={false}
            animate={{ stroke: saved ? color : "currentColor" }}
            transition={{ duration: 0.2 }}
          />
        </svg>
      </motion.button>
    </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 Mikrointeraktionen

Wofür du ihn nutzen kannst

Den Merken-Button drückt man, wenn man später zurückkommen will: ein Rezept, ein langer Artikel, eine Wohnung auf einem Immobilienportal, eine Stellenanzeige. Dieser füllt sich wie mit Flüssigkeit, das Band dehnt sich und federt, und ein kleines „Gespeichert“ schwebt kurz darüber. So ist die Aktion bestätigt, ganz ohne Toast.

Ein zweiter Tipp leert ihn schnell und ohne Federn, genau richtig fürs Rückgängigmachen. Das Gelb funktioniert auf hellen und dunklen Seiten, nimm aber deine Markenfarbe, wenn sie genug Kontrast hat. Den Hinweistext passt du an, etwa „Zu Favoriten hinzugefügt“.