ui.css · Cross-system: Cross-system reference § Content
<article> is the card primitive — self-contained content with title, meta, and optional
actions. Styling is applied directly to the element; no class is needed.
<article> — card container. A two-column grid (1fr auto): the body in
column 1, the actions in column 2. A direct child that is neither .card-media,
.card-link nor <menu> spans every column, so extra rows (a progress
strip, a disclosure panel) stack below the body at full width..card-media — optional leading picture, in a track of its own before the body.
Opt-in: article:has(> .card-media) is what adds the third track, so a
card without one keeps the two-column grid exactly as it was. Sized by
--card-media-size (4rem, 3rem at 640px and narrower) at
aspect-ratio: 2 / 3, and its <img> is
object-fit: contain — the whole image stays visible regardless of its proportions. It carries --shadow-2 to distinguish the image from the card. This is the strongest resting shadow; --shadow-1 looks too faint at this size. Placeholder tiles use the same shadow to keep lists visually consistent..card-link — the click target, wrapping the title and meta in column 1 across both
rows. It is itself a grid, so the lines inside it can truncate. The card's hover look is keyed off
it (see States). It reaches the whole card: a
::after at inset: 0 of the <article> stretches the
target over the padding, the gaps and the leading picture, so anywhere in the card that is not
another control activates this one link. Activating a pseudo-element targets the element it
belongs to, so the anchor navigates as usual and a handler reading
event.target.closest(…) sees the link rather than the box that was pressed. Two requirements are explained below: stretch only an active link, and keep other controls above its overlay. The link may also wrap a heading and a short description instead of .card-title and
.card-meta, for a card that links to another page and describes it. Its text becomes
the link's accessible name, so keep the description to a sentence or two, and put any link inside
the description in an extra row below instead..card-title — flex row with heading + inline badges; wraps on narrow screens, and its
h4 truncates a long title with an ellipsis..card-meta — muted metadata row: a wrapping flex row of <small>s
(icon + small text), so several facts sit side by side and wrap together instead of stacking.<menu> — the action slot: column 2 beside the body, list styling removed. Holds
icon buttons and the <ui-menu> trigger. Icon buttons here get a larger touch
target at 640px and narrower.
.card-media puts a picture of the card's subject in a track before the body. Draw the
tile even when there is no picture — an <ui-icon> as a placeholder keeps titles aligned so the list is easy to scan. Omitting the
element instead returns that card to the two-column grid, misaligning its title with the others.
Treat the picture as decorative unless it conveys information absent from the card text. Where it
restates a title already beside it, give it alt="" — otherwise a screen reader hears
every row twice. That is separate from .card-link's accessible-name rule below: a
.card-media outside the link is not part of the link's name, but it is still part of the
card's.
Keep .card-media beside the <a> so the image does not become part of the link's accessible name. The link's stretched click target already covers the image, so clicking the image and clicking the title perform the same action. Do not move the image inside the anchor just to make it clickable.
The card reacts only where it is actually clickable. Both hover effects — the darker
border and the brand-coloured h4 — are keyed off
article:has(> .card-link:hover, > .card-link:focus-visible), so a card whose text is not
wrapped in a .card-link has no hover effect. On clickable cards, the stretched link makes this effect respond across the card.
Only an active link or button receives the stretched target. The overlay is written
article > .card-link:is([href], [role="button"])::after. A card whose link is inert
— aria-disabled="true" with a title saying why, and neither
href nor role — keeps the small target and therefore stays inert
across its surface. Without this condition, the card would highlight on hover even though it cannot be opened. The text remains inside the anchor, whose title explains why the card is unavailable.
The focus ring surrounds the whole card. It is drawn by
article:has(> .card-link:is([href], [role="button"]):focus-visible) and the anchor's own
ring is dropped inside a card only, so a .card-link used outside one — a
side-panel row — keeps it. An indicator around the text while the whole card responds
does not show the full clickable area (WCAG 2.4.11 Focus Not Obscured / 2.4.13 Focus Appearance).
A non-interactive card needs no cursor override. The bare
article rule in ui.css sets no cursor at all, so a card
with nothing clickable in it resolves to auto on its own; the pointer comes from whatever is
clickable inside. Cards do not have cursor: pointer by default, so read-only lists need no cursor override.
Override both layout properties when using <article> as a plain container.
The rule sets display: grid and align-items: start. If only the first is overridden, rows can remain sized to their content instead of stretching across the card — a header using justify-content: space-between then has no extra width to distribute, and a footer’s border-top spans only its content. A wrapping metadata row can also retain its max-content width and cause horizontal overflow in the card’s container. Set align-items: stretch alongside the
display override, or keep the grid and let
& > :not(.card-link, menu) span both columns as the Anatomy section above describes.
<article> — screen readers list articles in the page's landmark list, so users can jump between cards.<a> or <button>, not the <article> itself. Otherwise screen readers announce the entire card text as the link label. .card-link is that inner target.<menu> — give any icon-only buttons an aria-label. The "More" button typically opens a <ui-menu>. Keep them out of .card-link: a link may not contain interactive content.<menu> and every extra row carry z-index: 1, which applies to grid items without setting position. Extra rows need this stacking level; otherwise, the overlay intercepts clicks intended for their links and buttons. It is also the reason a card is stretched by an overlay on the
inner <a> rather than by wrapping the card in one — a link may not contain the
card's own buttons, and wrapping would read every word of the card as the link's name.See Cross-system reference § Content for Bootstrap .card and Open UI card.research.