Ansora UI
Primitives

Ansora section card

A card for one section of a screen — heading with real heading semantics, optional description, a slot for a control, and an optionally scrollable body. The shell every data view on a dashboard sits in.

primitive

Install

pnpm dlx shadcn@4.23.0 add @ansora/section-card

Examples

Usage

title is a required string, not a ReactNode: upstream CardTitle renders <div data-slot="card-title"> with no asChild/render escape hatch, so this component patches heading semantics onto it (role="heading" aria-level={2}) — a visual heading with no semantics fails WCAG 1.3.1. title is required even when the heading is not shown: an accessible name is required, a visible one is not always wanted.

description, action and titleHidden form a discriminated union, the same trick StatusPill uses for children. With titleHidden absent or false (the default): description and action are both optional; action is a slot (a Select, a button, a filter), not configuration — this card does not know what a range is. With titleHidden: true: no CardHeader renders at all — not merely hidden, gone — title instead renders as an sr-only element that still carries role="heading" aria-level={2}, and the Card gets gap-0 py-0 so it hugs its content with no header left to put space between. description and action both fail to compile in that branch (description?: never, action?: never): neither has anywhere left to render once CardHeader is gone, so the type refuses the combination instead of silently dropping one. Reach for titleHidden when a section reads as a distinct block with no printed label of its own — @ansora/kpi-strip is the one consumer today, because its reference has no heading a sighted user can see, only four numbers; that is strictly better than rendering no heading at all, since the scroll region below still needs a real accessible name (WCAG) and a screen-reader user still needs a heading to jump to.

scroll turns the body into a horizontal scroll region; it is { label: string }, not a boolean, so a scroller cannot be switched on without naming it — an unnamed focus stop is announced as an anonymous box, and one with nothing focusable inside is unreachable by keyboard at 200% zoom (WCAG 1.4.10 / 2.1.1). The body renders with no horizontal padding (px-0 on CardContent) — on purpose, so a table can run edge to edge, with its row hover and row separator full-bleed to the card's own border; content that wants the inset back asks for it itself with className="px-(--card-p)", do not remove the px-0 to get it.

import { SectionCard } from "@/components/ui/ansora/section-card"

<SectionCard title="Pacienti" description="Za posledních 30 dní" action={<Select />}>
  <DataTable ... />
</SectionCard>

<SectionCard title="Klíčové ukazatele" titleHidden scroll={{ label: "Klíčové ukazatele" }}>
  <KpiCells />
</SectionCard>

Files

Dependencies

  • @ansora/base
  • https://ui.shadcn.com/r/styles/base-nova/card.json

On this page