Replied to 4 threads this week
UserCard
One member, summarised. The Network lists (followers, following, connections,
find investors), the activity feed's bylines, and the dashboard's
recommendation and profile-snapshot widgets each render a flavour of this
card. UserCard standardises the two halves they share:
- Header — always shown. Avatar, name with badges, one line of quiet facts (location · mutual connections · member since), an optional recent-activity line, and the actions (Connect, Message, Follow, an overflow menu).
- Details — opt-in. The standardized "more about this member" block: stat tiles (posts, votes, doors, mutuals), titled sections of link rows (latest forum posts, portfolio), or a disclosure that keeps all of that behind a "Show more" trigger. In the card layout it sits under a hairline divider.
In atomic-design terms it is a molecule: it composes Avatar, Button,
Menu, and Lucide glyphs, and owns only layout and the interactive parts
react-aria provides (Name and Item are RAC Links; Disclosure wraps
RAC's Disclosure).
import { UserCard } from "@pockets-ui/components"Anatomy
| Part | Role |
|---|---|
UserCard.Root | The surface (article). Takes size (normal, compact), layout (card, inline), and variant (outline, elevated). |
UserCard.Header | The always-shown row: Avatar · Identity · Actions. |
UserCard.Avatar | Frame for an Avatar.Root, sized to match the card. |
UserCard.Identity | The flexible middle: NameRow on the first line, Metas inline on the second, Activity on the third. |
UserCard.NameRow | Name followed by Badges; wraps. |
UserCard.Name | RAC Link to the profile. Plain at rest, underlined on hover. |
UserCard.Badge | Static chip beside the name: role, tier, relationship. Takes intent. |
UserCard.Meta | One quiet fact. Adjacent Metas share a line, separated by a middle dot. |
UserCard.Activity | Recent-activity line on its own row. Blue glyph; text truncates to one line. |
UserCard.Actions | Buttons and an overflow menu, pinned to the trailing edge. |
UserCard.Details | The standardized "more" block: a hairline-divided block (card) or an indented continuation (inline). |
UserCard.Stats | Hairline grid of Stat tiles. |
UserCard.Stat | One tile: StatValue over StatLabel, optional glyph pinned to the corner. |
UserCard.StatValue | The pre-formatted number. |
UserCard.StatLabel | What it counts. |
UserCard.Section | A titled group of Item rows. |
UserCard.SectionTitle | The section's eyebrow (a p, not a heading). |
UserCard.Item | RAC Link row: a thread, a property, a shared interest. |
UserCard.Disclosure | RAC Disclosure; owns the expanded state and stamps data-expanded. |
UserCard.DisclosureTrigger | RAC Button (slot="trigger") rendered as a text button with a chevron. Defaults to "Show more". |
UserCard.DisclosurePanel | RAC DisclosurePanel; hidden while collapsed. |
Basic
The default card: size="normal", layout="card". Pair it with an md
avatar and leave the avatar's alt empty — the name is right beside it. Put
each fact in its own Meta; they flow onto one line with dots between them,
so the card stays three lines tall. Keep Actions to the two or three things
a member does from a list. Destructive actions (Unfollow, Remove connection)
belong in an overflow Menu, where they take a second, more deliberate click.
Code
import { Avatar, Button, Menu, UserCard } from "@pockets-ui/components"
import { Activity, Ellipsis } from "lucide-react"
import { button } from "@/styled-system/recipes"
// Menu.Trigger renders its own element, so style it with the button recipe.
const overflow = button({ variant: "ghost", size: "sm" })
export default function UserCardBasic() {
return (
<UserCard.Root css={{ maxWidth: "2xl" }}>
<UserCard.Header>
<UserCard.Avatar>
<Avatar.Root>
<Avatar.Image
src="https://api.dicebear.com/9.x/shapes/svg?seed=ada"
alt=""
/>
<Avatar.Fallback>AL</Avatar.Fallback>
</Avatar.Root>
</UserCard.Avatar>
<UserCard.Identity>
<UserCard.NameRow>
<UserCard.Name href="#ada">Ada Lovelace</UserCard.Name>
<UserCard.Badge intent="warning">Agent</UserCard.Badge>
</UserCard.NameRow>
<UserCard.Meta>Denver, CO</UserCard.Meta>
<UserCard.Meta>3 mutual connections</UserCard.Meta>
<UserCard.Activity>
<Activity />
<span>Replied to 4 threads this week</span>
</UserCard.Activity>
</UserCard.Identity>
<UserCard.Actions>
<Button size="sm" variant="outline">
Connect
</Button>
<Button size="sm" variant="outline">
Message
</Button>
<Menu.Root>
<Menu.Trigger
className={overflow}
aria-label="More actions for Ada Lovelace"
>
<Ellipsis aria-hidden="true" size={16} />
</Menu.Trigger>
<Menu.Content aria-label="More actions">
<Menu.Item id="unfollow">Unfollow</Menu.Item>
<Menu.Item id="remove">Remove connection</Menu.Item>
</Menu.Content>
</Menu.Root>
</UserCard.Actions>
</UserCard.Header>
</UserCard.Root>
)
}Badges
Badge is a static chip. intent picks the wash from the semantic
intents — the suggested mapping is roles (Agent, Lender) → warning,
Pro → premium, a business account → brand, a relationship to the viewer
(Connected, Following) → success. Keep to two or three per card; the
row wraps but the eye doesn't.
Code
import { Avatar, UserCard } from "@pockets-ui/components"
export default function UserCardBadges() {
return (
<UserCard.Root css={{ maxWidth: "2xl" }}>
<UserCard.Header>
<UserCard.Avatar>
<Avatar.Root>
<Avatar.Fallback>GH</Avatar.Fallback>
</Avatar.Root>
</UserCard.Avatar>
<UserCard.Identity>
<UserCard.NameRow>
<UserCard.Name href="#grace">Grace Hopper</UserCard.Name>
<UserCard.Badge intent="premium">Pro</UserCard.Badge>
<UserCard.Badge intent="warning">Lender</UserCard.Badge>
<UserCard.Badge intent="success">Connected</UserCard.Badge>
</UserCard.NameRow>
<UserCard.Meta>Commercial lender at Hopper Capital</UserCard.Meta>
<UserCard.Meta>Arlington, VA</UserCard.Meta>
</UserCard.Identity>
</UserCard.Header>
</UserCard.Root>
)
}Details
Details is the standardized place for everything beyond the header, so
richer cards on the dashboard and the basic cards in a list stay recognisably
the same component. In the card layout it sits under a hairline divider. It stacks
Stats (a hairline grid of the profile's posts / votes / doors / rank tiles)
and Sections of Item rows (the latest forum posts, a portfolio strip).
Pre-format numbers yourself.
1,204
Posts
3,918
Votes
14
Doors
#3
Contributor
Code
import { Avatar, Button, UserCard } from "@pockets-ui/components"
import {
ArrowBigUp,
Building2,
FileText,
Home,
MessageSquare,
Trophy,
} from "lucide-react"
const posts = [
{ id: 1, title: "Is a 6% cap rate still worth it in Denver?" },
{ id: 2, title: "How I screened my first out-of-state PM" },
]
export default function UserCardDetails() {
return (
<UserCard.Root css={{ maxWidth: "2xl" }}>
<UserCard.Header>
<UserCard.Avatar>
<Avatar.Root>
<Avatar.Fallback>KJ</Avatar.Fallback>
</Avatar.Root>
</UserCard.Avatar>
<UserCard.Identity>
<UserCard.NameRow>
<UserCard.Name href="#katherine">Katherine Johnson</UserCard.Name>
<UserCard.Badge intent="premium">Pro</UserCard.Badge>
</UserCard.NameRow>
<UserCard.Meta>Hampton, VA</UserCard.Meta>
<UserCard.Meta>Member since 2016</UserCard.Meta>
</UserCard.Identity>
<UserCard.Actions>
<Button size="sm">Connect</Button>
</UserCard.Actions>
</UserCard.Header>
<UserCard.Details>
<UserCard.Stats>
<UserCard.Stat>
<MessageSquare />
<UserCard.StatValue>1,204</UserCard.StatValue>
<UserCard.StatLabel>Posts</UserCard.StatLabel>
</UserCard.Stat>
<UserCard.Stat>
<ArrowBigUp />
<UserCard.StatValue>3,918</UserCard.StatValue>
<UserCard.StatLabel>Votes</UserCard.StatLabel>
</UserCard.Stat>
<UserCard.Stat>
<Home />
<UserCard.StatValue>14</UserCard.StatValue>
<UserCard.StatLabel>Doors</UserCard.StatLabel>
</UserCard.Stat>
<UserCard.Stat>
<Trophy />
<UserCard.StatValue>#3</UserCard.StatValue>
<UserCard.StatLabel>Contributor</UserCard.StatLabel>
</UserCard.Stat>
</UserCard.Stats>
<UserCard.Section>
<UserCard.SectionTitle>Latest forum posts</UserCard.SectionTitle>
{posts.map((post) => (
<UserCard.Item key={post.id} href={`#post-${post.id}`}>
<FileText />
{post.title}
</UserCard.Item>
))}
</UserCard.Section>
<UserCard.Section>
<UserCard.SectionTitle>Portfolio</UserCard.SectionTitle>
<UserCard.Item href="#portfolio">
<Building2 />
Added a fourplex in Norfolk, VA · Mar 2026
</UserCard.Item>
</UserCard.Section>
</UserCard.Details>
</UserCard.Root>
)
}Disclosure
When a list would get too tall with every card's details open, keep them
behind a Disclosure. RAC owns the expanded state — uncontrolled with
defaultExpanded, or controlled with isExpanded + onExpandedChange — and
wires aria-expanded / aria-controls on the trigger. The chevron rotates on
the disclosure's data-expanded.
Posted 2 days ago
Code
import { Avatar, Button, UserCard } from "@pockets-ui/components"
import { Activity, FileText } from "lucide-react"
export default function UserCardDisclosure() {
return (
<UserCard.Root css={{ maxWidth: "2xl" }}>
<UserCard.Header>
<UserCard.Avatar>
<Avatar.Root>
<Avatar.Fallback>MH</Avatar.Fallback>
</Avatar.Root>
</UserCard.Avatar>
<UserCard.Identity>
<UserCard.NameRow>
<UserCard.Name href="#margaret">Margaret Hamilton</UserCard.Name>
<UserCard.Badge intent="warning">Property manager</UserCard.Badge>
</UserCard.NameRow>
<UserCard.Meta>Boston, MA</UserCard.Meta>
<UserCard.Activity>
<Activity />
<span>Posted 2 days ago</span>
</UserCard.Activity>
</UserCard.Identity>
<UserCard.Actions>
<Button size="sm" variant="outline">
Follow
</Button>
</UserCard.Actions>
</UserCard.Header>
<UserCard.Details>
<UserCard.Disclosure>
<UserCard.DisclosureTrigger>
More about Margaret
</UserCard.DisclosureTrigger>
<UserCard.DisclosurePanel>
<UserCard.Stats>
<UserCard.Stat>
<UserCard.StatValue>412</UserCard.StatValue>
<UserCard.StatLabel>Posts</UserCard.StatLabel>
</UserCard.Stat>
<UserCard.Stat>
<UserCard.StatValue>86</UserCard.StatValue>
<UserCard.StatLabel>Best replies</UserCard.StatLabel>
</UserCard.Stat>
<UserCard.Stat>
<UserCard.StatValue>7</UserCard.StatValue>
<UserCard.StatLabel>Mutuals</UserCard.StatLabel>
</UserCard.Stat>
</UserCard.Stats>
<UserCard.Section>
<UserCard.SectionTitle>Latest forum posts</UserCard.SectionTitle>
<UserCard.Item href="#post-1">
<FileText />
Tenant screening checklist for small multifamily
</UserCard.Item>
<UserCard.Item href="#post-2">
<FileText />
What does a good PM report look like?
</UserCard.Item>
</UserCard.Section>
</UserCard.DisclosurePanel>
</UserCard.Disclosure>
</UserCard.Details>
</UserCard.Root>
)
}Sizes and layouts
Two sizes and two layouts, freely combined:
size="compact"tightens padding and type for dense lists and grids. Pair it with ansmavatar andsize="sm"buttons;normalpairs withmd.layout="inline"drops the frame for a list of members inside a surface that already has one (aCard, a popover, a feed). Adjacent inline cards draw a hairline between themselves, andDetailsindents to the identity column so it reads as part of the same entry.variant(outline,elevated) picks the card surface and does nothing forlayout="inline".
Posted in Multifamily yesterday
128
Posts
6
Doors
9 yrs
Investing
Replied to your thread
128
Posts
6
Doors
9 yrs
Investing
Code
import { Avatar, Button, Card, UserCard } from "@pockets-ui/components"
import { Activity } from "lucide-react"
import { css } from "@/styled-system/css"
const stack = css({
display: "flex",
flexDirection: "column",
gap: "6",
width: "full",
maxWidth: "2xl",
})
const members = [
{
id: "rosalind",
initials: "RF",
name: "Rosalind Franklin",
badge: { intent: "brand", label: "Business" },
meta: ["Austin, TX", "12 mutual connections"],
activity: "Posted in Multifamily yesterday",
},
{
id: "dorothy",
initials: "DH",
name: "Dorothy Hodgkin",
badge: { intent: "success", label: "Connected" },
meta: ["Oxford, UK", "Buy & hold"],
activity: "Replied to your thread",
},
{
id: "lise",
initials: "LM",
name: "Lise Meitner",
badge: null,
meta: ["Stockholm, SE", "2 mutual connections"],
activity: "Added a duplex last week",
},
] as const
export default function UserCardVariants() {
return (
<div className={stack}>
{/* Compact card: for a dense grid of members, paired with a sm avatar. */}
<UserCard.Root size="compact" variant="elevated">
<UserCard.Header>
<UserCard.Avatar>
<Avatar.Root size="sm">
<Avatar.Fallback>RF</Avatar.Fallback>
</Avatar.Root>
</UserCard.Avatar>
<UserCard.Identity>
<UserCard.NameRow>
<UserCard.Name href="#rosalind">Rosalind Franklin</UserCard.Name>
<UserCard.Badge intent="brand">Business</UserCard.Badge>
</UserCard.NameRow>
<UserCard.Meta>Austin, TX</UserCard.Meta>
<UserCard.Meta>12 mutual connections</UserCard.Meta>
</UserCard.Identity>
<UserCard.Actions>
<Button size="sm">Connect</Button>
</UserCard.Actions>
</UserCard.Header>
</UserCard.Root>
{/* Inline, compact: a list of members inside a surface that already has
a frame. Adjacent cards draw their own hairline dividers. */}
<Card padding="none" css={{ paddingInline: "4" }}>
{members.map((member) => (
<UserCard.Root key={member.id} size="compact" layout="inline">
<UserCard.Header>
<UserCard.Avatar>
<Avatar.Root size="sm">
<Avatar.Fallback>{member.initials}</Avatar.Fallback>
</Avatar.Root>
</UserCard.Avatar>
<UserCard.Identity>
<UserCard.NameRow>
<UserCard.Name href={`#${member.id}`}>
{member.name}
</UserCard.Name>
{member.badge ? (
<UserCard.Badge intent={member.badge.intent}>
{member.badge.label}
</UserCard.Badge>
) : null}
</UserCard.NameRow>
{member.meta.map((line) => (
<UserCard.Meta key={line}>{line}</UserCard.Meta>
))}
</UserCard.Identity>
<UserCard.Actions>
<Button size="sm" variant="outline">
Follow
</Button>
</UserCard.Actions>
</UserCard.Header>
</UserCard.Root>
))}
</Card>
{/* Inline, normal: a feed. The details block indents to the identity
column so it reads as part of the same entry. */}
<div>
{members.slice(0, 2).map((member) => (
<UserCard.Root key={member.id} layout="inline">
<UserCard.Header>
<UserCard.Avatar>
<Avatar.Root>
<Avatar.Fallback>{member.initials}</Avatar.Fallback>
</Avatar.Root>
</UserCard.Avatar>
<UserCard.Identity>
<UserCard.NameRow>
<UserCard.Name href={`#${member.id}`}>
{member.name}
</UserCard.Name>
{member.badge ? (
<UserCard.Badge intent={member.badge.intent}>
{member.badge.label}
</UserCard.Badge>
) : null}
</UserCard.NameRow>
{member.meta.map((line) => (
<UserCard.Meta key={line}>{line}</UserCard.Meta>
))}
<UserCard.Activity>
<Activity />
<span>{member.activity}</span>
</UserCard.Activity>
</UserCard.Identity>
<UserCard.Actions>
<Button size="sm" variant="outline">
Message
</Button>
</UserCard.Actions>
</UserCard.Header>
<UserCard.Details>
<UserCard.Stats>
<UserCard.Stat>
<UserCard.StatValue>128</UserCard.StatValue>
<UserCard.StatLabel>Posts</UserCard.StatLabel>
</UserCard.Stat>
<UserCard.Stat>
<UserCard.StatValue>6</UserCard.StatValue>
<UserCard.StatLabel>Doors</UserCard.StatLabel>
</UserCard.Stat>
<UserCard.Stat>
<UserCard.StatValue>9 yrs</UserCard.StatValue>
<UserCard.StatLabel>Investing</UserCard.StatLabel>
</UserCard.Stat>
</UserCard.Stats>
</UserCard.Details>
</UserCard.Root>
))}
</div>
</div>
)
}Current user
A card for the viewer themselves drops the things that only make sense about
someone else: Actions, the mutual-connections line, the recent-activity
line. Omit href on Name and RAC renders it as a non-navigating span with
link semantics removed.
One-off overrides
Every part takes a css prop for style overrides that don't deserve a recipe
variant. A badge can be rethemed through its two custom properties without
forking the recipe (its hairline ring is mixed from the foreground, so it
follows):
<UserCard.Badge
css={{
"--pk-user-card-badge-bg": "token(colors.purple.100)",
"--pk-user-card-badge-fg": "token(colors.purple.800)",
}}
>
Pro
</UserCard.Badge>