Contact Card
Original · freeA contact card with avatar, role and quick action buttons.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/card-contact.json"use client";
import { useState } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
type Person = {
id: string;
name: string;
role: string;
company: string;
email: string;
phone: string;
img: string;
online: boolean;
};
const PEOPLE: Person[] = [
{
id: "amara",
name: "Amara Okafor",
role: "Principal Product Designer",
company: "Linear",
email: "amara.okafor@linear.app",
phone: "+1 (415) 555-0182",
img: "/img/gallery/g04.webp",
online: true,
},
{
id: "diego",
name: "Diego Marchetti",
role: "Staff Engineer, Platform",
company: "Vercel",
email: "diego.marchetti@vercel.com",
phone: "+1 (628) 555-0147",
img: "/img/gallery/g11.webp",
online: false,
},
{
id: "yuki",
name: "Yuki Tanaka",
role: "Head of Developer Relations",
company: "Stripe",
email: "yuki.tanaka@stripe.com",
phone: "+81 3-5555-0193",
img: "/img/gallery/g19.webp",
online: true,
},
];
function PhoneIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path
d="M6.5 3.5h3l1.2 4-2 1.4a12 12 0 0 0 5.4 5.4l1.4-2 4 1.2v3a2 2 0 0 1-2.2 2A16.5 16.5 0 0 1 4.5 5.7 2 2 0 0 1 6.5 3.5Z"
stroke="currentColor"
strokeWidth="1.6"
strokeLinejoin="round"
/>
</svg>
);
}
function MailIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<rect x="3.5" y="5.5" width="17" height="13" rx="2.2" stroke="currentColor" strokeWidth="1.6" />
<path d="m4.5 7 7.5 5.5L19.5 7" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
</svg>
);
}
function ChatIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path
d="M4.5 6.5A2 2 0 0 1 6.5 4.5h11a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H9l-4 3.2V15.5H6.5"
stroke="currentColor"
strokeWidth="1.6"
strokeLinejoin="round"
/>
</svg>
);
}
function CopyIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<rect x="8.5" y="8.5" width="10" height="10" rx="2" stroke="currentColor" strokeWidth="1.6" />
<path d="M5.5 15.5V6.5a2 2 0 0 1 2-2h8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
</svg>
);
}
function CheckIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path d="m5 12.5 4.2 4.2L19 7" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function StarIcon({ filled }: { filled: boolean }) {
return (
<svg viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} aria-hidden="true" className="h-4 w-4">
<path
d="m12 3.8 2.5 5.1 5.6.8-4 4 .9 5.6L12 16.7 6.9 19.3l1-5.6-4-4 5.6-.8L12 3.8Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
);
}
const iconBtn =
"inline-flex h-10 w-10 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-600 transition-all duration-150 hover:-translate-y-0.5 hover:border-slate-300 hover:text-slate-900 hover:shadow-sm active:translate-y-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:border-slate-600 dark:hover:text-white dark:focus-visible:ring-offset-slate-900";
function Avatar({ person, size }: { person: Person; size: "md" | "lg" }) {
const dim = size === "lg" ? "h-16 w-16" : "h-12 w-12";
const dot = size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5";
return (
<div className="relative shrink-0">
<span className="absolute inset-0 -z-10 rounded-full bg-gradient-to-br from-indigo-400/40 to-violet-400/40 blur-[6px] dark:from-indigo-500/30 dark:to-violet-500/30" />
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={person.img}
alt={person.name}
loading="lazy"
draggable={false}
className={`${dim} rounded-full object-cover ring-2 ring-white shadow-sm dark:ring-slate-800`}
/>
<span
className={`absolute -bottom-0.5 -right-0.5 ${dot} rounded-full ring-2 ring-white dark:ring-slate-800 ${
person.online ? "bg-emerald-500" : "bg-slate-300 dark:bg-slate-600"
}`}
aria-label={person.online ? "Online" : "Offline"}
role="img"
/>
</div>
);
}
function ContactCard({ person, featured }: { person: Person; featured?: boolean }) {
const reduce = useReducedMotion();
const [copied, setCopied] = useState(false);
const [saved, setSaved] = useState(false);
const copyEmail = async () => {
try {
await navigator.clipboard.writeText(person.email);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
} catch {
setCopied(false);
}
};
return (
<motion.article
initial={reduce ? false : { opacity: 0, y: 14 }}
whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
className={`group relative w-full overflow-hidden rounded-2xl border bg-white p-5 shadow-sm transition-shadow duration-200 hover:shadow-lg dark:bg-slate-900 ${
featured
? "border-indigo-200 ring-1 ring-indigo-100 dark:border-indigo-500/40 dark:ring-indigo-500/20"
: "border-slate-200 dark:border-slate-800"
}`}
>
<div
className="cc-shine pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-400/70 to-transparent"
aria-hidden="true"
/>
<div className="flex items-start gap-4">
<Avatar person={person} size="lg" />
<div className="min-w-0 flex-1 pt-0.5">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<h3 className="truncate text-base font-semibold text-slate-900 dark:text-white">{person.name}</h3>
<p className="truncate text-sm text-slate-500 dark:text-slate-400">{person.role}</p>
</div>
<button
type="button"
aria-pressed={saved}
aria-label={saved ? `Remove ${person.name} from favorites` : `Save ${person.name} to favorites`}
onClick={() => setSaved((s) => !s)}
className={`inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border transition-all duration-150 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
saved
? "border-amber-300 bg-amber-50 text-amber-500 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-400"
: "border-slate-200 bg-white text-slate-400 hover:text-amber-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-500"
}`}
>
<StarIcon filled={saved} />
</button>
</div>
<span className="mt-2 inline-flex items-center gap-1.5 rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600 dark:bg-slate-800 dark:text-slate-300">
<span
className={`h-1.5 w-1.5 rounded-full ${person.online ? "bg-emerald-500" : "bg-slate-400"}`}
aria-hidden="true"
/>
{person.company}
</span>
</div>
</div>
<dl className="mt-5 space-y-1 text-sm">
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-400">
<dt className="sr-only">Email</dt>
<MailIcon />
<dd className="truncate">{person.email}</dd>
</div>
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-400">
<dt className="sr-only">Phone</dt>
<PhoneIcon />
<dd className="tabular-nums">{person.phone}</dd>
</div>
</dl>
<div className="mt-5 flex items-center gap-2">
<a
href={`tel:${person.phone.replace(/[^\d+]/g, "")}`}
className="inline-flex flex-1 items-center justify-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-all duration-150 hover:-translate-y-0.5 hover:bg-indigo-500 hover:shadow-md active:translate-y-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
>
<PhoneIcon />
Call
</a>
<a
href={`mailto:${person.email}`}
aria-label={`Email ${person.name}`}
className={iconBtn}
>
<MailIcon />
</a>
<button type="button" aria-label={`Message ${person.name}`} className={iconBtn}>
<ChatIcon />
</button>
<button
type="button"
onClick={copyEmail}
aria-label={copied ? "Email copied to clipboard" : `Copy ${person.name}'s email`}
className={`${iconBtn} ${copied ? "!border-emerald-300 !text-emerald-600 dark:!border-emerald-500/40 dark:!text-emerald-400" : ""}`}
>
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={copied ? "check" : "copy"}
initial={reduce ? false : { scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={reduce ? undefined : { scale: 0.5, opacity: 0 }}
transition={{ duration: 0.15 }}
className="inline-flex"
>
{copied ? <CheckIcon /> : <CopyIcon />}
</motion.span>
</AnimatePresence>
</button>
</div>
</motion.article>
);
}
function CompactRow({ person }: { person: Person }) {
const [saved, setSaved] = useState(false);
return (
<div className="flex items-center gap-3 rounded-xl border border-slate-200 bg-white p-3 transition-colors hover:border-slate-300 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-slate-700">
<Avatar person={person} size="md" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold text-slate-900 dark:text-white">{person.name}</p>
<p className="truncate text-xs text-slate-500 dark:text-slate-400">
{person.role} · {person.company}
</p>
</div>
<a href={`mailto:${person.email}`} aria-label={`Email ${person.name}`} className={iconBtn}>
<MailIcon />
</a>
<button
type="button"
aria-pressed={saved}
aria-label={saved ? `Remove ${person.name} from favorites` : `Save ${person.name} to favorites`}
onClick={() => setSaved((s) => !s)}
className={`inline-flex h-10 w-10 items-center justify-center rounded-xl border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
saved
? "border-amber-300 bg-amber-50 text-amber-500 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-400"
: "border-slate-200 bg-white text-slate-400 hover:text-amber-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-500"
}`}
>
<StarIcon filled={saved} />
</button>
</div>
);
}
export default function CardContact() {
return (
<section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
<style>{`
@keyframes cc-shine-kf {
0%, 100% { opacity: 0.25; }
50% { opacity: 1; }
}
.cc-shine { animation: cc-shine-kf 3.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.cc-shine { animation: none; }
}
`}</style>
<div className="mx-auto max-w-5xl">
<header className="mx-auto mb-10 max-w-2xl text-center">
<span className="inline-flex items-center rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
Contact cards
</span>
<h2 className="mt-4 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-white">
Reach your people in one tap
</h2>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
Avatars, live status, and quick actions — call, email, message, or copy in a click.
</p>
</header>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{PEOPLE.map((p, i) => (
<ContactCard key={p.id} person={p} featured={i === 0} />
))}
</div>
<div className="mx-auto mt-8 max-w-md space-y-2">
<p className="mb-3 text-center text-xs font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
Compact variant
</p>
{PEOPLE.map((p) => (
<CompactRow key={`row-${p.id}`} person={p} />
))}
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
3D Tilt Cards
OriginalA responsive grid of cards that tilt in 3D toward your cursor with a light glare and lifted depth layers, powered by Framer Motion springs.

Spotlight Follow Cards
OriginalFeature cards where a soft radial spotlight and a glowing border chase the cursor on hover, with a staggered scroll reveal and an animated shimmer line.

3D Flip Cards
OriginalPricing-style cards that flip in 3D on hover or tap to reveal details on the back, keyboard accessible with a staggered entrance animation.

Animated Gradient Border Cards
OriginalCards wrapped in a live conic gradient border that rotates and speeds up on hover, with a shimmer sweep and a scrolling tag marquee.

Glow Effect Card
primitivesA reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card
gradient-card.tsxA reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card
card.tsxA reusable spotlight magic card for React and Tailwind, with hover and motion.

Tilt Spotlight Card
primitivesA reusable tilt spotlight card for React and Tailwind, with hover and motion.

Profile Card
OriginalA user profile card with avatar, stats and a follow toggle.

Product Card
OriginalA product card with image, price and add-to-cart feedback.

Pricing Single Card
OriginalA single pricing plan card with feature list and CTA.

Stat Card
OriginalA metric card with value, trend arrow and a small sparkline.

