Skip to content
goodtabs UI
BrowseBrowse components

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

MO

Mira Okafor

Design engineer

Lisbon, joined Mar 2024

Builds the component library and most of the motion on the site.

Avatar beside the text, two outline actions on the right.

Column, centred

TL

Theo Lindqvist

Founder

Stockholm, joined Jan 2023

Picks every resource in the library by hand.

Everything stacked and centred, for a team page.

In a list

MO

Mira Okafor

Design engineer

Lisbon, joined Mar 2024

TL

Theo Lindqvist

Founder

Stockholm, joined Jan 2023

AR

Ana Reyes

Frontend developer

Mexico City, joined Aug 2025

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

PropTypeDefaultDescription
user{ name: string; role?: string; image?: string; meta?: string; bio?: string }The person. Initials from name are the avatar fallback.
actionsReactNodeButtons 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