ComponentsLayout & themingCard
On this page
Layout & theming

Card released

A box for one thing on a dashboard: a title, a subtitle, an icon and actions above, anything in the body, and a footer that stays at the bottom. The whole card can be one link.

  • Import@pacit/components/card
  • Selectorspct-card, [pctCardTitle], [pctCardSubtitle], a[pctCardLink], [pctCardControl]

No APG pattern applies, and none is invented — the reasoning is under Accessibility ↓

Heat pump

Utility room

21.4 °C

card.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctCard, PctCardLink, PctCardTitle, PctCardSubtitle } from '@pacit/components/card';
ExportedWhat it is
PctCardLinkcomponentunreleaseda[pctCardLink]The anchor that makes the whole card one link: the consumer's <a> inside the title, with its own href or routerLink, whose box is stretched over the card by a pseudo-element.
PctCardControldirectiveunreleased[pctCardControl]A control that answers on its own inside a card that is one link: it stands over the link's box, so a press on it is its own and not the link's. The header's actions and the footer's actions stand there already; this is for a control written in the body, or for a passage whose text must stay selectable, which then leaves the link.
PctCardTitlecomponentunreleased[pctCardTitle]The card's title: the consumer's heading, at the level the page's outline needs, with the card's type on it and the browser's margins taken off.
PctCardSubtitlecomponentunreleased[pctCardSubtitle]The line under the card's title: where the thing stands, or what kind it is.
PctCardcomponentunreleasedpct-cardA card: a header of a title, a subtitle, an icon and actions, a body of anything, and a footer that stays at the bottom when the cards of a row stand at one height.
<pct-card>
  <h3 pctCardTitle><a pctCardLink routerLink="/devices/heat-pump">Heat pump</a></h3>
  <p pctCardSubtitle>Utility room</p>
  <p>21.4 °C</p>
  <span pctCardFooter>Updated a moment ago</span>
</pct-card>
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

API

What it reads, and what it writes back

Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.

unreleased marks 8 of them: on main, and not in @pacit/components@0.2.0 — the next release names the version.

A card: a header of a title, a subtitle, an icon and actions, a body of anything, and a footer that stays at the bottom when the cards of a row stand at one height.

Inputs

NameTypeWhat it does
loadingunreleaseddefault falsebooleanWhether the card's content is on its way: the body gives way to the skeleton, and the card is aria-busy until it arrives. The header and the footer stay.
errorunreleaseddefault nullstring | nullA message saying why the card has nothing to show, drawn in place of the body with the danger icon; null or '' for none. loading wins while both are set: the answer is not in yet.
mutedunreleaseddefault falsebooleanWhether the thing the card stands for is out of reach — a device that does not answer. The card stands on the skin's disabled surface and keeps its controls working; say why in the card's own words, because colours are the eye's alone.

Outputs

None of its own. The native events — (click), (focus), the form's submit — are the platform's, and two-way models above carry the rest.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-card
aria-busyloading() ? "true" : null
data-pct-statestate()
data-pct-mutedmuted() ? "" : null
data-pct-linklink() ? "" : null
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

In a test, PctCardHarness, PctCardTitleHarness, PctCardSubtitleHarness, PctCardLinkHarness, PctCardControlHarness from @pacit/components/testing read a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
actions[data-pct-part="actions"]the region of the header's controls, at its end
body[data-pct-part="body"]the region of the body, or of the skeleton while loading
footer[data-pct-part="footer"]the row at the bottom of the card
footer-actions[data-pct-part="footer-actions"]the region of the footer's controls
header[data-pct-part="header"]the row of the icon, the heading and the actions
heading[data-pct-part="heading"]the column of the title and the subtitle
icon[data-pct-part="icon"]the region of the consumer's icon, at the header's start
message[data-pct-part="message"]the error message drawn in place of the body
message-icon[data-pct-part="message-icon"]the danger icon beside the message
note[data-pct-part="note"]the region of the footer's text

Tokens

Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.

TokenStyles
--pct-card-bg{pct.surface}#ffffff · #0f172aBackground of the card.
--pct-card-bg-muted{pct.surface-disabled}#f1f5f9 · #1e293bBackground of a muted card.
--pct-card-border{pct.border}#e2e8f0 · #334155Edge colour of the card.
--pct-card-border-hover{pct.border-strong}#64748bEdge colour of a card that is one link, under the pointer.
--pct-card-fg{pct.text}#0f172a · #ffffffText colour of the card.
--pct-card-fg-muted{pct.text-muted}#334155 · #e2e8f0Text colour of a muted card.
--pct-card-footer-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the card's footer.
--pct-card-footer-font-size{pct.font.size.sm}13pxFont size of the card's footer.
--pct-card-gap{pct.space.4}12pxGap between the card's header, body and footer.
--pct-card-header-gap{pct.space.3}8pxGap between the icon, the heading and the actions in the header.
--pct-card-icon-color-danger{pct.danger}#b91c1c · #f87171Colour of the icon beside the card's error message.
--pct-card-message-bg-danger{pct.danger-100}#fee2e2 · #991b1bBackground of the card's error message.
--pct-card-message-fg-danger{pct.on-danger-100}#991b1b · #fee2e2Text colour of the card's error message.
--pct-card-padding-x{pct.space.5}16pxInline padding inside the card's edge.
--pct-card-padding-y{pct.space.5}16pxBlock padding inside the card's edge.
--pct-card-radius{pct.radius.md}8pxCorner radius of the card.
--pct-card-subtitle-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the card's subtitle.
--pct-card-subtitle-font-size{pct.font.size.sm}13pxFont size of the card's subtitle.
--pct-card-title-font-size{pct.font.size.lg}16pxFont size of the card's title.
--pct-card-title-font-weight{pct.font.weight.medium}500Font weight of the card's title.

Theming

A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.

[data-theme='brand'] {
  --pct-card-border: #99f6e4;
  --pct-card-border-hover: #0f766e;
  --pct-card-radius: 16px;
}

Heat pump

Utility room

21.4 °C

Accessibility

Measured, not assumed

Pattern: none — the APG has no pattern for a card. The card writes no role; its heading is the consumer's, at the level the page's outline needs, and a card that is one link is the consumer's <a> in that heading. Named in the class JSDoc.

Keyboard

keyeffecttest
Tab / Shift+Tabmoves to the card's link, then to its controls in orderapps/sandbox-e2e/src/card.spec.ts — "the keyboard meets the link, then the controls, and Enter follows the link"
Enteron the link, follows itthe same case; the following is the platform's

There are no handlers to map: the link is an anchor and the controls are the consumer's, and each answers the keyboard on its own.

What is measured, and what is not yet

Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.

MeasuredARIA pattern named in the class JSDoclibs/components/card/src/card.ts
MeasuredKeyboard map tested key by keythe table above — every row measured in apps/sandbox-e2e/src/card.spec.ts
Measuredaxe audit on the component's own sandbox viewapps/sandbox-e2e/src/a11y.spec.ts — /card in SBX_ROUTES, the card that is one link and the three states included
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — card-grid and card-grid-rtl
Measuredforced-colors: active — no state carried by colour alonelibs/components/card/src/card.scss and card-link.scss — the edge CanvasText, a muted card GrayText, the error a box of its own, the link's ring Highlight. The reading: apps/sandbox-e2e/src/forced-colors.spec.ts
n/aprefers-reduced-motion — duration from a tokennot applicable — the card moves nothing; the skeleton it draws while loading follows its own card
n/aTouch target ≥ 24×24 px outrightnot applicable — the card's targets are the consumer's controls and the link, whose box is the whole card
MeasuredSize axis aligned to --pct-control-height-*deliberately not: a card is not a control, and it is sized by its content and the spacing scale
MeasuredDensity axislibs/tokens/src/component.card.json — the padding and the gaps stand on space.3, space.4 and space.5, which the compact scope re-points
MeasuredRTL — no physical properties + a dir="rtl" screenshotlogical properties only, held by check-styles; the screenshot card-grid-rtl, where the actions move to the left
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /card in SBX_ROUTES
n/aFormsnot applicable — a card holds no value a form owns
MeasuredParts registered in the inventorylibs/components/parts.snapshot.md — the ten parts above
MeasuredTokens registered + an entry in contrast.policy.jsonlibs/tokens/src/contrast.policy.json — nine entries: the text, the subtitle, the footer, the muted text and the error message over their surfaces (AA error), the error icon (UI error), the edges (UI warn); libs/tokens/tokens.snapshot.md
n/aStrings through PCT_TEXTSnot applicable — the component writes no word of its own
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./card row
MeasuredScreen-reader logRead 2026-11-02 from docs/acr/at/ — three readers, whole sandbox. One link, named by the title, and then the controls: Orca Heat pump link · heading 3., then Enabled · switch pressed. and Rename · button.; NVDA Heat pump, heading, with no role word for the link, then Enabled, switch, on and Rename, button. VoiceOver reached the card's switch alone in both walks of the day (the first, which lost /text, was not kept), Enabled on switch, and lost the page after it, so it has not read the link
MeasuredA docs page with live examplesapps/sandbox/src/app/views/card/ (the sandbox view). The published site: /components/card, its demos' source in the code tab
MeasuredUnit + mutationlibs/components/card/src/card.spec.ts; libs/components/mutation.snapshot.md — card.ts, measured by a narrow run
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

100%mutation score5 of 5 mutants killed
9 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
9colour pairs measuredlight and dark, at every token build
2visual baselinesChromium, the sandbox's own pictures
13elements in the preview0 listeners · 1 render to settle · 1787 µs in jsdom, held exactly but for the clock

Known limitations

  • The text of a card that is one link cannot be selected. A drag over it selects nothing and a double click follows the link, in all three engines (0109). pctCardControl on a passage takes it out of the link.
  • A control in the body needs pctCardControl. Without it the control sits under the link's box and a press on it is the link's. A control is anything the consumer writes, so nothing can warn about one that was missed.
  • pctCardControl writes two inline declarations. position: relative and z-index: 1 stand on the element whether or not the card holds a link, so they override a position of the consumer's own, and two raised controls stack in their order in the document.
  • The card is its link's containing block. It is position: relative while it holds a link, so an absolutely positioned child of the card is placed against the card.
  • No elevation and no media slot. The card is a part of the page with an edge, not a sheet lifted over it; a picture is the consumer's element in the body.
  • NVDA does not say the link is a link. In the walk of 2026-11-02 NVDA with Firefox read the focused title as Heat pump, heading, where Orca says link. The element is an <a href> in an <h3>, and Enter follows it; whether the stretched box changes what NVDA reads is for the next walk to measure against a plain link in a heading.
  • An error is not announced. The message is drawn, not spoken: a page of cards loading at once would raise a role="alert" per card that failed. A failure the user is waiting for is the application's to say, with a toast or the announcer.
  • Muted is a colour. It says nothing to a reader; the card's words must say why.