Back

Animated Border Button

An animated outline button with dynamic border transitions, icon feedback, and smooth state changes from action to success.

Category
ButtonReact
CSS
shadcn

Manual

Create a file and paste the following code into it.

src/index-demo.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import { useState, useRef, useEffect } from "react";
import { AnimatePresence, motion } from "motion/react";
import { TextMorph } from "torph/react";

import * as Button from "@/components/ui/button";

import TrashIcon from "@/components/ui/icons/trash";
import SuccessIcon from "@/components/ui/icons/success";

export default function Demo() {
  const [loading, setLoading] = useState(false);
  const [success, setSuccess] = useState(false);

  const loadingTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);

  const handleSuccess = () => {
    if (loading || success) return;

    setLoading(true);

    loadingTimeout.current = setTimeout(() => {
      setLoading(false);
      setSuccess(true);
    }, 2000);
  };

  useEffect(() => {
    if (!success) return;

    const id = setTimeout(() => {
      setSuccess(false);
    }, 2000);

    return () => clearTimeout(id);
  }, [success]);

  useEffect(() => {
    return () => {
      if (loadingTimeout.current) {
        clearTimeout(loadingTimeout.current);
      }
    };
  }, []);

  return (
    <section className="flex h-dvh w-full items-center justify-center gap-4 px-6">
      <Button.Root
        variant={success ? "success" : "error"}
        mode="animatedBorder"
        size="medium"
        onClick={handleSuccess}
        animateBorder={loading}
        showAnimatedBorder={loading}
        animatedBorderStyle={loading ? "dashed" : "solid"}
        disabled={loading}
      >
        <AnimatePresence mode="popLayout">
          <motion.div
            key={success ? "success" : "remove"}
            initial={false}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.4, y: 10 }}
            transition={{ duration: 0.2, ease: "easeOut" }}
          >
            <Button.Icon
              as={success ? SuccessIcon : TrashIcon}
              className="size-5"
              aria-hidden
            />
          </motion.div>
        </AnimatePresence>
        <TextMorph>{success ? "Success" : "Remove"}</TextMorph>
      </Button.Root>
    </section>
  );
}

Update the import paths to match your project setup.

Similar screens