import { AnimatePresence, motion } from "framer-motion"
const buttonCopy = {
idle: "Send me a login link",
loading: <Spinner size={16} color="rgba(255, 255, 255, 0.65)" />,
success: "Login link sent!",
}
function SmoothButton() {
const [buttonState, setButtonState] = React.useState("idle")
return (
<div className="outer-wrapper">
<button
disabled={buttonState !== "idle"}
onClick={() => {
// This code is just a placeholder
setButtonState("loading")
setTimeout(() => {
setButtonState("success")
}, 1750)
setTimeout(() => {
setButtonState("idle")
}, 3500)
}}
>
{/* `wait` ensures the exiting elements finishes its animation before the entering element animates in. NOTE: this should be `popLayout` actually but it has a bug when used in shadow roots, which is how I isolate styles in my React demos: https://github.com/framer/motion/issues/2508 */}
{/* `initial` prevents the animation from running on first render */}
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={buttonState}
initial={{ opacity: 0, y: -25 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 25 }}
transition={{ type: "spring", duration: 0.25, bounce: 0 }}
>
{buttonCopy[buttonState]}
</motion.span>
</AnimatePresence>
</button>
</div>
)
}
function Spinner({ color, size = 20 }) {
const bars = Array(12).fill(0)
return (
<div
className="wrapper"
style={{
["--spinner-size"]: `${size}px`,
["--spinner-color"]: color,
}}
>
<div className="spinner">
{bars.map((_, i) => (
<div className="bar" key={`spinner-bar-${i}`} />
))}
</div>
</div>
)
}
const styles = (
<style>{`
button {
border-radius: 8px;
font-weight: 500;
font-size: 13px;
height: 32px;
min-width: 148px;
overflow: hidden;
background: linear-gradient(180deg, #1994ff 0%, #157cff 100%);
box-shadow:
0px 0px 1px 1px rgba(255, 255, 255, 0.08) inset,
0px 1px 1.5px 0px rgba(0, 0, 0, 0.32),
0px 0px 0px 0.5px #1a94ff;
position: relative;
}
button span {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
color: white;
text-shadow: 0px 1px 1.5px rgba(0, 0, 0, 0.16);
}
.outer-wrapper {
display: flex;
padding: 120px 40px;
justify-content: center;
}
.wrapper {
height: var(--spinner-size, 20px);
width: var(--spinner-size, 20px);
}
.spinner {
position: relative;
top: 50%;
left: 50%;
height: var(--spinner-size, 20px);
width: var(--spinner-size, 20px);
}
.bar {
animation: spin 1.2s linear infinite;
background: var(--spinner-color);
border-radius: 6px;
height: 8%;
left: -10%;
position: absolute;
top: -3.9%;
width: 24%;
}
.bar:nth-child(1) {
animation-delay: -1.2s;
transform: rotate(0.0001deg) translate(146%);
}
.bar:nth-child(2) {
animation-delay: -1.1s;
transform: rotate(30deg) translate(146%);
}
.bar:nth-child(3) {
animation-delay: -1s;
transform: rotate(60deg) translate(146%);
}
.bar:nth-child(4) {
animation-delay: -0.9s;
transform: rotate(90deg) translate(146%);
}
.bar:nth-child(5) {
animation-delay: -0.8s;
transform: rotate(120deg) translate(146%);
}
.bar:nth-child(6) {
animation-delay: -0.7s;
transform: rotate(150deg) translate(146%);
}
.bar:nth-child(7) {
animation-delay: -0.6s;
transform: rotate(180deg) translate(146%);
}
.bar:nth-child(8) {
animation-delay: -0.5s;
transform: rotate(210deg) translate(146%);
}
.bar:nth-child(9) {
animation-delay: -0.4s;
transform: rotate(240deg) translate(146%);
}
.bar:nth-child(10) {
animation-delay: -0.3s;
transform: rotate(270deg) translate(146%);
}
.bar:nth-child(11) {
animation-delay: -0.2s;
transform: rotate(300deg) translate(146%);
}
.bar:nth-child(12) {
animation-delay: -0.1s;
transform: rotate(330deg) translate(146%);
}
@keyframes spin {
0% {
opacity: 1;
}
100% {
opacity: 0.15;
}
}
`}</style>
)
export default function Demo() {
return (
<>
{styles}
<SmoothButton />
</>
)
}