Section Card
Padded page-body shell that owns its title, tools, and actions so the name is never a sibling outside the card.
Section Card is the page-body shell: one padded card that owns its title, optional count, tools, and actions. Pass the name as title — never render an h2 above an untitled card. This page is the component (install, props, examples). Which shell to pick, title vs group heading, and rhythm live on Page Sections. Object Page padding, Descriptions layout, and toolbar control height live on Object Page Cards.
Related List and Formily FormPanel variant="card" compose this shell — do not wrap them in another card.
When To Use
- Any page-body region that needs a card: Attachments, Notes, Line items, a shared Contact + Fulfillment block.
- Put Refresh / Columns in
toolsand the region primary (Add) inactions— one row with the title. See CRUD Page Patterns — Region Table Toolbar. - Use Related List when the body is a loose child collection with Loading / Empty / Error.
- Use
FormPanel variant="card"when the body is a Form — it setsbodyGap="form"for you. - Preview / markdown that fills a column:
fitHeightonSectionCard(orFormPanel). Do not addoverflow-y-autoinside defaultp-6. - Do not hand-roll
rounded-xl border p-6or a sibling heading above a card. Do not nest Section Card inside Section Card.
Features
| Area | Behavior |
|---|---|
| Shell | p-6 padded section (rounded-xl border bg-card). Not the p-4 Statistic tile. |
| Title | title renders an h2 at 16px semibold. Omit title when peer groups share one untitled card. |
| Groups | SectionGroupHeading is muted 14px. Titled card → groups are h3; untitled → groups are h2 so h1 never skips. |
| Toolbar | Title + optional count on the left; tools then actions on the right. No border-b under the title. |
| Gap | bodyGap="dense" (default, gap-4) for tables, lists, Descriptions; "form" inherits --fui-form-field-gap. |
| Bleed | bodyPadding="none" pulls the body to the card edges — escape hatch, not the Table recipe. |
| Fill height | fitHeight fills the parent; the body scrolls flush to the card border (Cloudscape Container). Use for preview / markdown. Do not use for Related List / tables, and do not put overflow-y-auto on a child of default p-6. |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/section-card.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/section-card.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/section-card.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/section-card.jsonWith a namespace: npx shadcn@latest add @f-ui/section-card.
registryDependencies: shadcn utils, https://ui.isaacfei.com/r/section-surface.json.
Usage
import {
SectionCard,
SectionGroupHeading,
} from "@/components/f-ui/section-card/section-card";
<SectionCard title="Order details">
<SectionGroupHeading>Contact</SectionGroupHeading>
{/* fields */}
</SectionCard>Examples
Title and In-Card Groups
The card name is Order details (h2). Contact and Fulfillment stay muted group headings (h3) with a Separator between them. Composition rules: Page Sections.
Order details
Contact
- Owner
- Mei Chen
- mei.chen@example.com
Fulfillment
- Carrier
- SF Express
- Ship by
- 2026-03-08
"use client";
import {
SectionCard,
SectionGroupHeading,
} from "@/components/f-ui/section-card/section-card";
import { Separator } from "@/components/ui/separator";
function ContactFields() {
return (
<dl className="grid grid-cols-[8rem_1fr] gap-x-4 gap-y-2 text-sm">
<dt className="text-muted-foreground">Owner</dt>
<dd>Mei Chen</dd>
<dt className="text-muted-foreground">Email</dt>
<dd>mei.chen@example.com</dd>
</dl>
);
}
function FulfillmentFields() {
return (
<dl className="grid grid-cols-[8rem_1fr] gap-x-4 gap-y-2 text-sm">
<dt className="text-muted-foreground">Carrier</dt>
<dd>SF Express</dd>
<dt className="text-muted-foreground">Ship by</dt>
<dd>2026-03-08</dd>
</dl>
);
}
/** Titled card owns the h2; in-card groups stay muted. */
export function SectionCardDemo() {
return (
<SectionCard title="Order details">
<div className="flex flex-col gap-3">
<SectionGroupHeading>Contact</SectionGroupHeading>
<ContactFields />
</div>
<Separator />
<div className="flex flex-col gap-3">
<SectionGroupHeading>Fulfillment</SectionGroupHeading>
<FulfillmentFields />
</div>
</SectionCard>
);
}Title Row Tools and Add
Ghost Refresh lives in tools on the same row as the title and count. Add is the only solid control in actions. Do not stack tools in a corner. Playbook: CRUD Page Patterns — Region Table Toolbar. Object Page toolbar height: Object Page Cards.
Members
2- Ada Lovelaceada@example.comOwner
- Grace Hoppergrace@example.comMember
"use client";
import { RefreshCwIcon } from "lucide-react";
import { List } from "@/components/f-ui/list/list";
import { SectionCard } from "@/components/f-ui/section-card/section-card";
import { Button } from "@/components/ui/button";
import { IconGhostButton } from "@/demos/_shared/icon-ghost-button";
/** Title, count, ghost Refresh, and one solid Add share a single toolbar row. */
export function SectionCardToolbarDemo() {
return (
<SectionCard
title="Members"
count={2}
tools={
<IconGhostButton label="Refresh">
<RefreshCwIcon className="size-4" />
</IconGhostButton>
}
actions={<Button type="button">Add members</Button>}
>
<List>
<List.Item extra="Owner">
<List.Meta description="ada@example.com" title="Ada Lovelace" />
</List.Item>
<List.Item extra="Member">
<List.Meta description="grace@example.com" title="Grace Hopper" />
</List.Item>
</List>
</SectionCard>
);
}Fill-Height Body Scroll
The parent is a fixed-height flex column. fitHeight keeps Preview on the title row and makes the body the scrollport — the scrollbar hugs the card edge. Type is long on purpose; do not wrap the markdown in a second overflow-y-auto. Tables and Related Lists stay on the default padded shell.
Preview
"use client";
import { SectionCard } from "@/components/f-ui/section-card/section-card";
const SAMPLE = Array.from({ length: 12 }, (_, i) =>
`Paragraph ${i + 1}. Preview markdown and long reads scroll inside the card body. The scrollbar sits on the rounded border, not inset by padding.`,
).join("\n\n");
/** Fill-height preview: title stays; body is the flush scrollport. */
export function SectionCardFitHeightDemo() {
return (
<div className="flex h-80 min-h-0 flex-col">
<SectionCard title="Preview" fitHeight>
<p className="text-sm whitespace-pre-wrap">{SAMPLE}</p>
</SectionCard>
</div>
);
}Composition
SectionCard
├── SectionGroupHeadingLevel (titled → groups are h3; untitled → h2)
└── section
├── header?
│ ├── toolbar (title + count | tools + actions)
│ └── description?
└── body (children; `fitHeight` → overflow on the bled body, pad on an inner wrap)SectionTitle is the h2 used when title is set. SectionGroupHeading reads the level from context — do not pass a heading level.
API Reference
Props
SectionCard
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | The section's only semibold name (h2). Omit for peer groups sharing one untitled card. |
description | ReactNode | — | Muted supporting line under the title row. |
count | ReactNode | — | Muted counter on the title baseline. |
tools | ReactNode | — | Low-emphasis cluster (Refresh, Columns). Same row as the title. |
actions | ReactNode | — | Region primary on the title row. At most one solid button. |
bodyGap | "dense" | "form" | "dense" | Title → body air. "form" inherits the enclosing Form field gap. |
bodyPadding | "default" | "none" | "default" | "none" bleeds the body to the card edges (non-scroll media). Ignored when fitHeight is on. |
fitHeight | boolean | false | Fill the parent; body scrolls flush to the border. Preview / markdown only — not Related List / tables. |
headerClassName | string | — | Extra classes on the header block. |
bodyClassName | string | — | Extra classes on the body. Put group rhythm here — do not override gap-* on the shell. |
className | string | — | Merged onto the root section. Do not pass p-6 — the shell already has it. |
children | ReactNode | — | Section body. |
Also accepts native <section> attributes except title.
SectionGroupHeading
Muted in-card group name. Heading level comes from the enclosing SectionCard (titled → h3, untitled → h2). Accepts native heading attributes plus className.
SectionTitle
The 16px semibold h2 used by the shell. Prefer the title prop on SectionCard / FormPanel unless you are composing a plain (non-card) panel.
SectionGroupHeadingLevel
Publishes the group-heading level for a titled vs untitled shell. FormPanel variant="plain" uses this; hosts almost never need it.
Hook
useSectionGroupHeadingLevel() returns 2 or 3 from the nearest SectionGroupHeadingLevel. SectionGroupHeading already consumes it.
See Also
- Page Sections — when to wrap, title vs group, rhythm
- Object Page Cards — Object Page padding, Descriptions, toolbar height
- Related List — child collection shell on this card
- Formily —
FormPanel variant="card"for form sections - CRUD Page Patterns — region table toolbar