Animated copy to clipboard button with Framer Motion and SVG

Created Sep 26 2020

JS
1
import { css } from '@emotion/core';
2
import { motion, useMotionValue, useTransform } from 'framer-motion';
3
import React from 'react';
4
5
export const CopyToClipboardButton = props => {
6
const duration = 0.4;
7
const boxVariants = {
8
hover: isChecked => ({
9
scale: 1.05,
10
strokeWidth: 3,
11
opacity: isChecked ? 0 : 1,
12
}),
13
pressed: isChecked => ({
14
scale: 0.95,
15
strokeWidth: 1,
16
opacity: isChecked ? 0 : 1,
17
}),
18
checked: { opacity: 0 },
19
unchecked: { stroke: '#949699', strokeWidth: 2, opacity: 1 },
20
};
21
22
const tickVariants = {
23
pressed: isChecked => ({ pathLength: isChecked ? 0.85 : 0.05 }),
24
checked: { pathLength: 1 },
25
unchecked: { pathLength: 0 },
26
};
27
28
const [isChecked, setIsChecked] = React.useState(false);
29
const pathLength = useMotionValue(0);
30
const opacity = useTransform(pathLength, [0.05, 0.15], [0, 1]);
31
32
const copyToClipboard = content => {
33
const el = document.createElement(`textarea`);
34
el.value = content;
35
el.setAttribute(`readonly`, ``);
36
el.style.position = `absolute`;
37
el.style.left = `-9999px`;
38
document.body.appendChild(el);
39
el.select();
40
document.execCommand(`copy`);
41
document.body.removeChild(el);
42
};
43
44
React.useEffect(() => {
45
if (isChecked) {
46
setTimeout(() => setIsChecked(false), 3000);
47
}
48
}, [isChecked]);
49
50
return (
51
<button
52
css={css`
53
background: transparent;
54
border: none;
55
height: 25px;
56
cursor: ${isChecked ? 'default' : 'pointer'};
57
outline: none;
58
`}
59
aria-label="Copy to clipboard"
60
title="Copy to clipboard"
61
disabled={isChecked}
62
onClick={() => {
63
copyToClipboard(props.text);
64
setIsChecked(true);
65
}}
66
>
67
<motion.svg
68
initial={false}
69
animate={isChecked ? 'checked' : 'unchecked'}
70
whileHover="hover"
71
whileTap="pressed"
72
transition={{ duration }}
73
width="25"
74
height="25"
75
viewBox="0 0 25 25"
76
fill="none"
77
xmlns="http://www.w3.org/2000/svg"
78
>
79
<motion.path
80
d="M20.8511 9.46338H11.8511C10.7465 9.46338 9.85107 10.3588 9.85107 11.4634V20.4634C9.85107 21.5679 10.7465 22.4634 11.8511 22.4634H20.8511C21.9556 22.4634 22.8511 21.5679 22.8511 20.4634V11.4634C22.8511 10.3588 21.9556 9.46338 20.8511 9.46338Z"
81
stroke="black"
82
strokeWidth="2"
83
strokeLinecap="round"
84
strokeLinejoin="round"
85
variants={boxVariants}
86
custom={isChecked}
87
transition={{ duration }}
88
/>
89
<motion.path
90
d="M5.85107 15.4634H4.85107C4.32064 15.4634 3.81193 15.2527 3.43686 14.8776C3.06179 14.5025 2.85107 13.9938 2.85107 13.4634V4.46338C2.85107 3.93295 3.06179 3.42424 3.43686 3.04917C3.81193 2.67409 4.32064 2.46338 4.85107 2.46338H13.8511C14.3815 2.46338 14.8902 2.67409 15.2653 3.04917C15.6404 3.42424 15.8511 3.93295 15.8511 4.46338V5.46338"
91
stroke="black"
92
strokeWidth="2"
93
strokeLinecap="round"
94
strokeLinejoin="round"
95
variants={boxVariants}
96
custom={isChecked}
97
transition={{ duration }}
98
/>
99
<motion.path
100
d="M20 6L9 17L4 12"
101
stroke="#949699"
102
strokeWidth="2"
103
strokeLinecap="round"
104
strokeLinejoin="round"
105
variants={tickVariants}
106
style={{ pathLength, opacity }}
107
custom={isChecked}
108
transition={{ duration }}
109
/>
110
</motion.svg>
111
</button>
112
);
113
};