Profile card
A person with avatar, name, role, a mono meta line, a bio and outline actions.
npx shadcn@latest add https://ui.goodtabs.co/r/profile-card.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/profile-card
Avatar beside the text, two outline actions on the right.
Column, centred
Everything stacked and centred, for a team page.
In a list
Three row cards stacked with a small gap.
Usage
import { ProfileCard } from "@/components/ui/profile-card"
import { Button } from "@/components/ui/button"
<ProfileCard
user={{ name: "Mira Okafor", role: "Design engineer", meta: "Lisbon, joined Mar 2024" }}
actions={<Button variant="outline" size="sm">Follow</Button>}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| user | { name: string; role?: string; image?: string; meta?: string; bio?: string } | The person. Initials from name are the avatar fallback. | |
| actions | ReactNode | Buttons on the right in a row, under the text in a column. Made pills. | |
| layout | "row" | "column" | "row" | Avatar beside the text, or everything stacked and centred. |
Source
profile-card/profile-card.tsx
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
export type ProfileUser = {
/** Full name. Its initials are the avatar fallback. */
name: string;
/** Role or title under the name. */
role?: string;
/** Avatar image url. */
image?: string;
/** A mono meta line, for example "Lisbon, joined Mar 2024". */
meta?: string;
/** One line about the person. */
bio?: string;
};
type ProfileCardProps = Omit<React.ComponentProps<typeof Card>, "children"> & {
/** The person to show. */
user: ProfileUser;
/** Buttons on the right in a row, under the text in a column. Buttons are made pills. */
actions?: React.ReactNode;
/** row puts the avatar beside the text; column stacks and centres everything. */
layout?: "row" | "column";
};
function initials(name: string) {
return name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("");
}
/**
* A person: avatar, name, role, a mono meta line, a bio and actions.
* Row for lists and headers, column for a team page.
*/
export function ProfileCard({ user, actions, layout = "row", className, ...props }: ProfileCardProps) {
const column = layout === "column";
return (
<Card
data-slot="profile-card"
data-layout={layout}
className={cn("surface-shine rounded-lg", className)}
{...props}
>
<CardContent className={cn("flex gap-4", column ? "flex-col items-center px-6 py-2 text-center" : "items-start")}>
<Avatar className="size-12">
{user.image && <AvatarImage src={user.image} alt="" />}
<AvatarFallback className="text-base text-foreground">{initials(user.name)}</AvatarFallback>
</Avatar>
<div className={cn("flex min-w-0 flex-col gap-1", column ? "items-center" : "flex-1")}>
<div className="flex flex-col">
<p className="m-0 truncate text-sm leading-5 font-medium text-foreground">{user.name}</p>
{user.role && <p className="m-0 truncate text-sm text-muted-foreground">{user.role}</p>}
</div>
{user.meta && <p className="m-0 truncate font-mono text-xs text-muted-foreground">{user.meta}</p>}
{user.bio && <p className="m-0 text-sm text-muted-foreground">{user.bio}</p>}
</div>
{actions && (
<div
className={cn(
"flex shrink-0 flex-wrap items-center gap-2 [&_[data-slot=button]]:rounded-full",
column ? "justify-center" : "justify-end",
)}
>
{actions}
</div>
)}
</CardContent>
</Card>
);
}
For AI agents
Hand your agent this page, the markdown at /components/profile-card/prompt.md, or the Copy AI prompt button above.
When to use
- Team and about pages, one card per person
- A member row in settings, with the actions on the right
- The author block under a post or a changelog entry
When not to use
- A tiny inline mention, which wants an avatar and a name only
- Tables of many members, where a data table scans faster
- Editing a profile, which wants a form
Agent notes
- Server component with no state; pass buttons as actions and handle clicks in the parent
- The avatar is 48px; pass user.image for a photo, the initials show while it loads or when it is missing
- Buttons inside actions are made rounded-full; use variant outline and size sm to match the demos
- Name, role and meta truncate on one line in the row layout; the bio wraps