Resolved: light

Lists & rows

The dense-row family — the building blocks of the portal's grouped lists, “Go to” shortcuts and tool menus. There are two row primitives: ListRow, the lightweight single-line(+subtitle) navigation/list row (the everyday list item), and CompactListItem, a heavier content-with-an-action row (always an icon, a semibold title, a real Button action that wraps below the text on mobile). Both are designed to stack inside a CompactCard's divide-y body — the card supplies the dividers, the header rule and the rounded-corner clip; each row carries only its own px-4 inset. Every surface here is theme-aware (colors resolve from the Save the Dogs CSS vars) — flip the theme in the top bar to confirm the rows, dividers and hover fills re-skin.

<ListRow>
The lightweight list row: optional icon chip + title (+ optional subtitle) + optional trailing action, in a compact or standard density. Set href to make the WHOLE row a next/link with a hover background and an auto chevron.
<CompactListItem>
The heavier content-with-an-action row: a REQUIRED icon chip + a semibold title (+ optional subtitle) on the left and an action node (typically a Button) on the right that stacks below the text on mobile, sits to the right from sm: up.
<CompactCard>
The grouped-list surface (see Cards & surfaces): a padding="none" Card with an optional SectionHeader header band over a divide-y body. Fill the body with ListRow / CompactListItem children — it owns the dividers + corner clip.
density
ListRow only — compact (py-2) vs standard (py-2.5, the default). CompactListItem has a single fixed density (py-2.5). The dividers and px-4 inset are identical across densities; only the vertical breathing room changes.
icon chip
Both rows render the icon in an h-9 w-9 rounded-lg bg-surface-2 text-muted chip. ListRow's icon is OPTIONAL (omit for a text-only row); CompactListItem's icon is REQUIRED.
no shadow, no own divider
These are inner rows, not surfaces — no border, no shadow, no divider of their own. They get their separation from the CompactCard (--color-rule divide-y). Standalone, they're framed here so the inset + hover read.

ListRow — anatomy

A single fully-loaded ListRow, left to right: the icon chip (h-9 w-9 bg-surface-2), the text column (title — text-sm font-medium, truncated; subtitle — text-xs muted, truncated), and the trailing action (here a Badge). The text column is min-w-0 flex-1 so a long title truncates rather than shoving the action off the row. Framed on the canvas so the px-4 inset is visible.

Sunrise Beagle Brigade
Downtown chapter · 24 members
Active
icon + title + subtitle + action (a Badge)
A deliberately very long team name that will not fit on one line and must therefore truncate with an ellipsis
A subtitle that is also far too long to fit and is likewise clamped to a single truncated line
Featured
long title truncates — action stays pinned right
icon
ReactNode (optional). Rendered in an h-9 w-9 rounded-lg bg-surface-2 text-muted chip on the left. Pass a sized lucide glyph (h-4 w-4). Omit for a text-only row.
title
ReactNode (required). The primary line — text-sm font-medium text-[--color-text], truncated. Sits in a min-w-0 flex-1 column so it shrinks before the action does.
subtitle
ReactNode (optional). The second line — text-xs text-muted, also truncated to one line. Omit for a single-line row.
action
ReactNode (optional). The trailing affordance, kept in a shrink-0 box on the right. Pass a Badge, a Button, custom text — or use href's auto chevron instead.

ListRow — density (compact vs standard)

The density prop is the headline knob: "compact" = py-2, "standard" = py-2.5 (the default). It changes ONLY the row's vertical padding — the px-4 inset, the icon chip, the type sizes and the dividers are identical. Use compact for tall scannable lists where you want more rows per screen; standard for the everyday navigation block. Both stacks below carry three identical rows so the height difference is the only variable.

Your teams
2 active communities
Upcoming events
Next: Saturday march
Your representatives
By your address
density="compact" — py-2 (tighter)
Your teams
2 active communities
Upcoming events
Next: Saturday march
Your representatives
By your address
density="standard" (default) — py-2.5
density="compact"
py-2 (8px top + bottom). The dense option — for long, scannable lists where you want more rows in view. Subtitle still fits; just less air around each row.
density="standard"
py-2.5 (10px top + bottom). The DEFAULT (omit the prop to get this). The everyday navigation / “Go to” block.
what's unchanged
Both densities keep the same px-4 horizontal inset, the same h-9 w-9 icon chip, the same text-sm / text-xs type, and the same --color-rule dividers. Only vertical padding moves.

ListRow — trailing affordance

What sits on the right edge. With href and no explicit action, a chevron appears automatically (the “go to” quick-link cue). Pass an action node (Badge, Button, plain text) to put something specific there instead. Pass action={null} to explicitly suppress the auto chevron on a link row — distinct from leaving action undefined. Each example is the same row body with a different right edge.

auto chevron — href set, action omitted
Data-access agreement
Required before exporting a roster
To sign
action={<Badge>} — a status badge on the right
City council WhatsApp
42 members
action={<Button>} — an inline action button
Notifications
Unread alerts in your inbox
3 new
action — plain trailing text (e.g. a count / meta)
action={null} — link row with the chevron suppressed
Just a title — text-only, single line
no icon, no subtitle, no action — bare title row
auto chevron
When href is set AND action is undefined, a ChevronRight (h-4 w-4 text-faint) is shown automatically and tints to text-accent on hover — the standard whole-row link cue.
action (node)
Any node — Badge, Button, count text — rendered in a shrink-0 box on the right. Setting it replaces the auto chevron even on a link row.
action={null}
An intentional opt-out: a link row (href set) with NO trailing affordance at all — the chevron is suppressed. Distinct from undefined (which keeps the auto chevron).
text-only
icon and subtitle are both optional — omit them for a bare single-line title row (useful for simple grouped lists where the icon would be noise).

ListRow — standalone vs inside a CompactCard

How ListRow is meant to ship. A single row has no border, no shadow and no divider of its own — so standalone it needs a frame (left). The portal pattern (right) drops the same rows into a CompactCard, which supplies the SectionHeader band, the --color-rule divide-y between rows, and the overflow-hidden corner clip. Same rows, two contexts.

standalone — a single bare row (needs its own frame)
inside a CompactCard — header band + divide-y body
self-supplied inset
Each ListRow carries its own px-4 (and density py-*). That's why it drops straight into a CompactCard body without extra wrapping — and why standalone it needs a frame to look intentional.
dividers come from the card
ListRow has NO divider of its own. The CompactCard body is a divide-y divide-[--color-rule] stack — the rule lines belong to the parent, so a lone row is rule-less.
corner clip
CompactCard is overflow-hidden, so the rows (and their hover fills) clip to the rounded-lg corners. Reproduce that with overflow-hidden when you frame a row standalone.

CompactListItem — content + action row

The heavier sibling of ListRow, for content-WITH-an-action rows (the YourGroupsCard “Join the WhatsApp group” pattern). Differences from ListRow: the icon is REQUIRED, the title is text-sm font-semibold (heavier), the action is a real Button (not a chevron), and the whole row is responsive — the action stacks BELOW the text on mobile (flex-col) and sits to the right from sm: up (sm:flex-row sm:items-center sm:justify-between). There is one fixed density (py-2.5); it has no density prop. Like ListRow it carries its own px-4 inset and no divider, so it composes inside a CompactCard.

Downtown organizers
128 members · very active
icon + title + subtitle + action (a Button)
Volunteer drivers
no subtitle — icon + title + action only
Saturday neighborhood march
June 28 · 10:00 AM · City Hall
action is a Link-styled / secondary Button
Sunrise Beagle Brigade
You're a member
no action — icon + content only (action omitted)
A very long community group name that overruns the row and is clamped to a single truncated line so the action stays put
A subtitle long enough to also need truncation on a narrow row
long title truncates; action keeps its space
icon
ReactNode (REQUIRED — unlike ListRow). Rendered in the same h-9 w-9 rounded-lg bg-surface-2 text-muted chip on the left.
title
ReactNode (required). text-sm font-SEMIBOLD (heavier than ListRow's font-medium), truncated. The content headline.
subtitle
ReactNode (optional). text-xs text-muted, truncated — the one-line supporting detail (member count, date, status).
action
ReactNode (optional). Typically a real Button (or a Link-wrapped Button). Stacks BELOW the text on mobile, sits to the right from sm: up. Omit for a content-only row.
responsive layout
flex-col gap-3 on mobile → sm:flex-row sm:items-center sm:justify-between sm:gap-4. The action is self-start on mobile, self-auto from sm: — narrow the viewport to see it wrap.
fixed density
py-2.5 + px-4, no density prop — it always aligns with a STANDARD-density ListRow when the two are stacked together in one CompactCard.

CompactListItem — inside a CompactCard

The shipping pattern: a CompactCard whose body is a divide-y stack of CompactListItems — the YourGroupsCard “Your communities” surface. The card supplies the header (here a custom SectionHeader with an icon), the dividers and the corner clip; each item brings its icon, title, subtitle and Join button. Because the item's density is py-2.5 it lines up cleanly with standard ListRows in a mixed list.

Your communities

3 groups
Downtown organizers
128 members · very active
Volunteer drivers
34 members
Adoption events crew
You're already a member
custom SectionHeader + a divide-y stack of items
header (custom node)
Pass a SectionHeader node as CompactCard's header to add an icon + hint (a string header would render a plain SectionHeader without the icon). The card draws the border-b rule beneath it.
divide-y body
Each CompactListItem is one row in the card's divide-[--color-rule] body — the dividers belong to the card. Items need no margins of their own; their px-4 / py-2.5 inset does the spacing.
mixing with ListRow
A CompactCard can hold both CompactListItems and standard-density ListRows — they share the px-4 inset and py-2.5 height, so a mixed list stays vertically aligned.

ListRow vs CompactListItem — which row?

The two rows side by side with the same content, so the deliberate differences read at a glance: ListRow (left) is the lightweight navigation row — medium-weight title, a chevron (or compact action), tuned for whole-row links and dense lists. CompactListItem (right) is the content-with-an-action row — semibold title, a real Button that wraps responsively, tuned for “here's a thing, do something with it”.

ListRow — lightweight, link/chevron, has density
Downtown organizers
128 members
CompactListItem — semibold, Button action, responsive
title weight
ListRow → text-sm font-medium. CompactListItem → text-sm font-SEMIBOLD. The heavier weight signals “content”, not just a nav label.
icon
ListRow → OPTIONAL. CompactListItem → REQUIRED. Reach for CompactListItem when every row genuinely has an icon and an action.
trailing affordance
ListRow → an auto chevron (whole-row link) or a compact action node. CompactListItem → a real Button (or Link-Button) that wraps below the text on mobile.
density
ListRow → compact | standard (py-2 / py-2.5). CompactListItem → fixed py-2.5 (aligns with standard ListRow).
rule of thumb
Navigating somewhere / scanning a dense list → ListRow. Presenting an item with a clear single action (Join, RSVP, Open) → CompactListItem.