UI
Contact button
Hover-reveal copy email CTA
Client · Motion
Live preview
Interact with the component below — same behavior as on the About page.
Summary
A primary CTA button that swaps between a compact “Contact” label and the full email address on hover or focus. Click copies the address to the clipboard with a check icon confirmation. Built with Motion layout animations and a clipboard fallback for older browsers.
- Layout animation between compact and expanded states
- Copy-to-clipboard with success feedback icon
- Focus-visible expand matches hover behavior
- Configurable email via prop
Add to your project
Run the CLI to copy files into your project. Pick your language and styling below — the command updates automatically.
1. Add via CLI (recommended)
npx @shashank-portfolio/cli add contact-button2. Install dependencies
npm install motion lucide-reactRequired packages
- motion ^12.0.0
- lucide-react ^0.562.0 · Mail, Copy, Check icons
3. Manual installation
Copy the file below. Pass email as a prop, or change the default import from siteConfig to your own constant.
contact-button.tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
import { Check, Copy, Mail } from "lucide-react";
import { useState } from "react";
import { siteConfig } from "@/lib/metadata";
import type { ReactNode } from "react";
const EASE = [0.22, 1, 0.36, 1] as const;
type ContactButtonProps = {
email?: string;
};
export function ContactButton({
email = siteConfig.email,
}: ContactButtonProps): ReactNode {
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const handleCopy = async (): Promise<void> => {
try {
await navigator.clipboard.writeText(email);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
} catch {
const ta = document.createElement("textarea");
ta.value = email;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try {
document.execCommand("copy");
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
} catch {}
document.body.removeChild(ta);
}
};
return (
<motion.button
type="button"
layout
onClick={handleCopy}
onHoverStart={() => setOpen(true)}
onHoverEnd={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
aria-label={
copied ? "Email copied" : open ? `Copy ${email}` : "Show email"
}
transition={{ layout: { duration: 0.55, ease: EASE } }}
style={{ borderRadius: 12 }}
className="focus-ring relative inline-flex h-11 cursor-pointer items-center justify-center bg-foreground px-5 text-sm font-medium text-background"
>
<motion.span
layout="position"
className="relative inline-flex items-center"
>
<AnimatePresence initial={false} mode="popLayout">
{open ? (
<motion.span
key="email"
layout="position"
initial={{ opacity: 0, filter: "blur(8px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(8px)" }}
transition={{ duration: 0.35, ease: EASE }}
className="inline-flex items-center gap-2 whitespace-nowrap"
>
<span className="relative inline-flex h-4 w-4 shrink-0 items-center justify-center">
<AnimatePresence initial={false} mode="wait">
{copied ? (
<motion.span
key="check"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.5, opacity: 0 }}
transition={{ duration: 0.2, ease: EASE }}
className="inline-flex"
>
<Check className="h-4 w-4" aria-hidden="true" />
</motion.span>
) : (
<motion.span
key="copy"
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.5, opacity: 0 }}
transition={{ duration: 0.2, ease: EASE }}
className="inline-flex"
>
<Copy className="h-4 w-4" aria-hidden="true" />
</motion.span>
)}
</AnimatePresence>
</span>
<span className="tabular-nums">{email}</span>
</motion.span>
) : (
<motion.span
key="contact"
layout="position"
initial={{ opacity: 0, filter: "blur(8px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(8px)" }}
transition={{ duration: 0.35, ease: EASE }}
className="inline-flex items-center gap-2 whitespace-nowrap"
>
<Mail className="h-4 w-4 shrink-0" aria-hidden="true" />
<span>Contact</span>
</motion.span>
)}
</AnimatePresence>
</motion.span>
</motion.button>
);
}
4. Use in your app
"use client";
import { ContactButton } from "@/components/contact/contact-button";
export function HeroCtas() {
return <ContactButton email="you@example.com" />;
}Let’s connect
I’m always open to discussing new projects, creative ideas, or opportunities to be part of your visions. Just reach out!