- Email: emma.carter@northwindtech.com
- Phone: +1 415 555 0132
- Location: San Francisco, CA
- Decision maker
- 2027 budget
Data Display
Contact card
v1.0.0Added in 1.0.0
A React contact card: a person or company at a glance with avatar, role, status, call, email and WhatsApp buttons, tags and last activity.
Tap Call, Email or WhatsApp to open the phone, the mail app or a WhatsApp chat (wa.me with the Romanian 40 prefix added to 07… numbers); an onCall / onEmail / onMessage handler replaces the link, for a softphone or an in-app composer, and the email and phone in the full card go through the same handlers. Tap the name (or anywhere on a linked card) to open the record: the card tints on hover and dips slightly while pressed (a row only tints), but neither dips nor darkens when the press lands on a button or link inside. The company, email, phone and website are underlined links of their own. The star saves the contact optimistically and rolls back, with a spoken error, when the request fails. The copy buttons next to email, phone and CUI copy the value and confirm with a check; they show on hover or keyboard focus of their row with a mouse and are always visible on touch. The ⋯ button opens the actions menu, a bottom sheet with the contact's name on phones. On narrow cards the buttons stack their icon over the label; in the row layout they appear on hover (always on touch), rows narrower than 30rem keep only the first one (Call), below 18rem none, and the row's ⋯ menu always lists every channel on top. Tab reaches the name link, the company link, the star, the menu, each contact button, then each field link and copy button, in reading order, with the focus ring drawn around the whole card for the name; Enter or Space activates; in the menu the arrows move and Escape closes it and returns focus to ⋯. Hover a round button for its label; long-press it on touch.
Contact card is a Pro React component in the React data display components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add contact-card, together with Avatar, Badge, Button, Copy Button, Dropdown Menu and 10 more and the npm packages lucide-react and motion.
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add contact-cardContact card is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Import ContactCard from @/components/ui/contact-card.
import { Pencil, PhoneCall, Trash2 } from "lucide-react"
import { ContactCard, type ContactData } from "@/components/ui/contact-card"
const emma: ContactData = {
id: "c-emma",
name: "Emma Carter",
title: "Head of Procurement",
company: { name: "Northwind Tech Inc.", href: "/clients/c01" },
email: "emma.carter@northwindtech.com",
phone: "+1 415 555 0132",
whatsapp: true,
location: "San Francisco, CA",
status: { label: "Active client", tone: "success" },
tags: [{ label: "Decision maker" }, { label: "2027 budget", tone: "info" }],
owner: { name: "Anna Price", avatar: "/demo/ana-pop.jpg" },
lastActivity: { label: "Call", at: new Date(2026, 8, 24, 15, 55), icon: <PhoneCall /> },
href: "/contacts/emma-carter",
}
export function ContactPanel({ onFavorite }: { onFavorite: (favorite: boolean) => Promise<unknown> }) {
return (
<ContactCard
contact={emma}
defaultFavorite={false}
onFavoriteChange={(favorite) => onFavorite(favorite)}
actions={[
{ label: "Edit", icon: <Pencil /> },
{ type: "separator" },
{ label: "Delete contact", icon: <Trash2 />, tone: "danger" },
]}
locale="en-US"
labels={{ owner: "Account owner" }}
/>
)
}contact*ContactData{ id; kind?: "person" | "company"; name; avatar?; title?; company?: { name; href?; logo? }; email?; phone?; whatsapp?: string | boolean (true uses phone); website?; location?; cui?; status?: { label; tone?; icon? }; tags?: { label; tone?; color? }[]; owner?: { name; avatar? }; lastActivity?: { label; at; icon? }; fields?: ContactFact[]; href? }. Anything missing simply does not render.
layoutdefault "full""full" | "compact" | "row"full: every part, for a record's side panel. compact: centered avatar, name, role, status and round buttons, for grids. row: one 56px line with icon buttons, for lists.
variantdefault "elevated""elevated" | "outline" | "soft" | "ghost"The card surface: a raised panel, a hairline frame, the flat gray tile or nothing (ghost, for rows inside a list group).
sizedefault "md""sm" | "md"md: a 48px avatar and a 17px name in the full layout (64px in compact); sm one step smaller.
radiusdefault "md""none" | "sm" | "md" | "lg"Card corners: none, 12px, 16px or 24px. Rows inside a list group use none.
tonedefault "neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Colors the selected ring and the star when it is on.
colordefault ""stringAny CSS color for the selected ring and the star. Replaces the tone.
defaultFavoritedefault falsebooleanThe star's starting state when uncontrolled. Setting it (or favorite, onFavoriteChange) shows the star.
favoritebooleanControlled star.
onFavoriteChange((favorite: boolean, contact: ContactData) => void | Promise<unknown>)Called when the star is toggled. Return a promise to keep it optimistic while pending; a rejection rolls it back and announces labels.favoriteError.
showFavoritedefault falsebooleanShows the star even without favorite props (an uncontrolled star that starts off).
selecteddefault falsebooleanA 2px ring in the tone and data-selected, for the picked contact in a grid (drawn inside the edge on rows, so a list group never clips it).
loadingdefault falsebooleanSkeletons in the layout's shape (aria-busy); the content fades in when it turns false.
showFieldsdefault truebooleanFull layout: the rows for email, phone, website, location and CUI, then contact.fields.
showTagsdefault truebooleanFull layout: the tag row.
showActivitydefault truebooleanFull layout: the footer with the owner and the last activity (a <time>, relative to now).
copyabledefault truebooleanCopy buttons on email, phone, CUI and facts with copyValue: on hover or focus with a mouse, always on touch.
channelsdefault "["call", "email", "whatsapp"]"("call" | "email" | "whatsapp" | "sms" | "website")[]Which contact buttons show, in order. A channel without data (no phone, no email) is hidden; with none left the row disappears.
onCall(contact: ContactData) => voidReplaces the tel: links of the Call button and the phone field (a softphone).
onEmail(contact: ContactData) => voidReplaces the mailto: links of the Email button and the email field (an in-app composer).
onMessage(contact: ContactData) => voidReplaces the wa.me and sms: links.
onChannelSelect(channel: ContactChannel, contact: ContactData, event: ContactChannelEvent) => voidRuns first for every contact button, every channel picked in the row menu and the email / phone / website links of the fields. event.preventDefault() stops the link and the handler (tracking, a confirmation).
actionsMenuEntry[]The ⋯ menu (Edit, Add note, Merge, Delete with tone "danger"), a bottom sheet on phones. Rows put the channels on top.
localedefault ""stringFormats the last activity time ("45 min. ago"). Empty uses the UIProvider locale, then the browser's.
nowDate | numberThe reference time for the relative activity time. Defaults to a live clock that ticks every minute.
linkComponent(props: LinkComponentProps) => ReactNodeRenders the name, company and fact links through your router's Link. Defaults to the LinkProvider's, else <a>.
labelsdefault "DEFAULT_CONTACT_CARD_LABELS"Partial<ContactCardLabels>call, email, whatsapp, sms, website, phone, location, copy, copied, favorite, favoriteAdd, favoriteRemove, favoriteError, actions, cancel, owner, lastActivity, cui, noActivity, tags (the tag list's name), actionLabel(action, name) ("Call Emma Carter"), copyField(field) ("Copy Email").
classNamesPartial<Record<"media" | "heading" | "name" | "title" | "company" | "status" | "favorite" | "menu" | "actions" | "action" | "fields" | "field" | "tags" | "footer" | "owner" | "activity", string>>Extra classes for the inner parts.
classNamestringExtra classes for the <article>, merged last.
The contact buttons alone, for other layouts: contact, channels, layout ("tiles" labelled buttons in equal columns, "icons" round buttons with tooltips), variant (soft), tone, radius, onCall / onEmail / onMessage / onChannelSelect, labels. Props: ContactActionsProps.
https://wa.me/40745123456 from "0745 123 456"; international numbers are kept as given, text becomes ?text=. Signature: (phone: string, text?: string) => string.
The English strings. Spread it and override a few keys, or pass a full translated set through labels. Type: ContactCardLabels.
tel:0745123456 from "0745 123 456", tel:+40745123456 from "+40 745 123 456": spaces and punctuation dropped, a leading + kept. Signature: (phone: string) => string.
A company card: logo initials in a rounded square, a tax ID (EIN) with copy, website and facts like the balance.
The client contacts in a 2-column phone / 4-column desktop grid, with one selected.
One 56px line per contact with favorites; buttons appear on hover and stay visible on touch.
Email and Call only, with onCall opening an in-app softphone instead of a tel: link.
Skeletons in the shape of each layout; the content fades in when it arrives.
Brand tone and a custom color paint the selected ring and the star.
An en-GB locale; a future activity reads as “in 2 hr”.
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
Run npx wingo-ui@latest add contact-card. It also adds the Wingo UI items it builds on (Avatar, Badge, Button, Copy Button, Dropdown Menu and 10 more) and the npm packages lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/contact-card.
The ⋯ button opens the actions menu, a bottom sheet with the contact's name on phones.
Tap a button: links are caught here
Links are intercepted here; the star saves with a fake request