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.
- 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.
- 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
- 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 — as a whole-row link (href)
When href is set the entire row becomes a next/link (internal navigation only) with a group class, a hover:bg-surface-2 background and — unless an action is supplied — the auto chevron, which tints to accent on hover. Hover any row below to see the fill + chevron respond. The hover fill is why a standalone link row needs the rounded + overflow-hidden frame: it clips the fill to the corners (in the portal the CompactCard does that). For external / tel: / mailto: links, pass an anchor via action instead — href is internal-only.
- href
- string (optional). Sets the whole row as a next/link: the row gets group + block + px-4 + transition-colors + hover:bg-surface-2. INTERNAL navigation only.
- hover fill
- hover:bg-surface-2 across the full row, plus the auto chevron tinting from text-faint to text-accent (group-hover). Frame standalone link rows with overflow-hidden so the fill clips to the corners.
- external links
- href is next/link (internal). For external URLs, tel: or mailto:, render your own anchor and pass it via action — don't put it in href.
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.
- 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.
- 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
- 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”.
- 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.