Skip to content

Card and Surface

LayoutSince 0.2.0
import { Card, Surface } from '@absoluteui/components/card';
import '@absoluteui/components/card/style.css';

Using Angular? See Card and Surface for Angular.

Card and Surface. .aui-surface is a bordered panel on color-surface, with flat, raised, and sunken variants and optional inset padding. .aui-card adds header (title, subtitle), media, body, and footer sections. Both work as CSS-only APIs.

Card.create renders the structure with the right semantics:

  • an <article> named by its title when there is one, otherwise a plain <div>;
  • an optional block-link mode, where the whole card is clickable, there is exactly one tab stop, and screen readers announce only the title.
  • Grouping a self-contained piece of content: a product, project, article teaser, or summary tile.
  • Collections of similar items in a grid, each linking to a detail page (interactive cards).
  • Panels that visually separate a region of a page (Surface).
  • A single clickable action without content: use a Button or Link.
  • Cards with several equally important actions: do not use interactive mode; put actions in the footer instead.
  • Tabular or comparable data across items: use a table.
  • Nesting cards inside cards.
import { Card } from '@absoluteui/components/card';
import { Button } from '@absoluteui/components/button';
const host = document.createElement('div');
document.body.append(host);
const archive = document.createElement('button');
archive.type = 'button';
archive.textContent = 'Archive';
Button.enhance(archive);
Card.create(host, {
title: 'Quarterly report',
subtitle: 'Updated today',
content: 'Revenue grew 4% compared to last quarter.',
footer: archive,
variant: 'raised',
});
import { Card } from '@absoluteui/components/card';
const host = document.createElement('div');
document.body.append(host);
const img = document.createElement('img');
img.src = '/images/alpha.png';
img.alt = '';
Card.create(host, {
title: 'Project Alpha',
subtitle: 'Owner: Ada Lovelace',
media: img,
interactive: { href: '/projects/alpha' },
headingLevel: 2,
});
import { Surface } from '@absoluteui/components/card';
const panel = document.createElement('section');
document.body.append(panel);
const surface = Surface.apply(panel, { variant: 'sunken', padded: true });
surface.update({ variant: 'raised' });
import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { card: { headingLevel: 2 } } });
import { Card } from '@absoluteui/components/card';
import '@absoluteui/components/card/style.css';
const host = document.querySelector<HTMLElement>('#report');
if (host) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Download';
const card = Card.create(host, { title: 'Quarterly report', content: 'Revenue grew 4%.', footer: button, variant: 'raised' });
button.addEventListener('click', () => {
card.update({ subtitle: 'Download started' });
});
}
<article class="aui-surface aui-card" aria-labelledby="c1">
<div class="aui-card__header"><h3 class="aui-card__title" id="c1">Quarterly report</h3></div>
<div class="aui-card__body">Revenue grew 4%.</div>
</article>
import { Card } from '@absoluteui/components/card';
const host = document.createElement('div');
document.body.append(host);
Card.create(host, { title: 'Project Alpha', subtitle: 'Owner: Ada', interactive: { href: '/projects/alpha' } });
<div> (consumer host)
<article class="aui-surface aui-card [aui-surface--raised|--sunken] [aui-card--interactive]" aria-labelledby=title>
<div class="aui-card__header">
<h3 class="aui-card__title" id=title>Title (h2-h6, headingLevel)
<a class="aui-card__link" href>Title</a> (interactive only; ::after covers the card)
</h3>
<p class="aui-card__subtitle">Subtitle</p> (optional)
</div>
<div class="aui-card__media"> (visually first; hidden while empty)
<div class="aui-card__body"> (content; hidden while empty)
<div class="aui-card__footer"> (actions/metadata; hidden while empty)
</article> (<div> when there is no title)
</div>
Card.create(host: HTMLElement, options?: CardOptions): Card; Surface.apply(el: HTMLElement, options?: SurfaceOptions): SurfaceHandle

Global configuration key: card (see Configuration).

Option Type Default Description
title string Card heading and the <article>’s accessible name (aria-labelledby). Without a title the card is a <div>.
subtitle string Supporting line under the title (text only).
content Node | string Body content; strings render as text. Omit to fill card.body directly.
footer Node Footer content such as actions or metadata.
media Node Image or illustration, shown above the header while the header stays first in source order.
interactive { href: string } Block-link mode: the title becomes the only link and its ::after covers the card. Requires title; href must be http(s) or relative.
variant 'flat' | 'raised' | 'sunken' "flat" Surface treatment (Card and Surface.apply).
padded boolean false Surface.apply only: space-inset padding.
headingLevel 2 | 3 | 4 | 5 | 6 3 Heading level of the card title; match it to the surrounding outline. Globally configurable.
Property Type Description
host HTMLElement Card: consumer host; its children are replaced by the card.
element HTMLElement Card: root <article> (titled) or <div>. Replaced when the title is added or removed.
body HTMLDivElement Card: body content area.
footer HTMLDivElement Card: footer area (hidden while empty).
media HTMLDivElement Card: media area (hidden while empty).
options Readonly<CardOptions> | Readonly<SurfaceOptions> Current options.
Method Description
update(options: Partial<CardOptions>): void Merge, validate, and apply options. Content areas are replaced only when their option is passed; pass undefined to remove title, subtitle, or interactive.
destroy(): void Card: remove the card element and the configuration subscription. SurfaceHandle: remove the classes it added.
Area Description
body card.body (or the content option).
footer card.footer (or the footer option).
media card.media (or the media option).

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-surface Bordered panel on color-surface with radius-md.
aui-surface--raised | --sunken | --padded Surface modifiers.
aui-card Card root (combined with aui-surface).
aui-card--interactive Block-link card: hover and focus-visible states on the card.
aui-card__header Title and subtitle.
aui-card__title Heading (h2 to h6).
aui-card__link Interactive title link; its ::after covers the card.
aui-card__subtitle Supporting text.
aui-card__media Media area, visually first.
aui-card__body Body content.
aui-card__footer Footer, a wrapping row.

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-focus-ring | --aui-color-link | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-muted | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-lg | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-semibold | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-shadow-md | --aui-shadow-sm | --aui-space-1 | --aui-space-2 | --aui-space-inset

type CardHeadingLevel = 2 | 3 | 4 | 5 | 6;
type SurfaceVariant = 'flat' | 'raised' | 'sunken';
interface CardInteractive { href: string }
interface CardOptions {
title?: string; subtitle?: string; content?: Node | string; footer?: Node; media?: Node;
interactive?: CardInteractive; variant?: SurfaceVariant; headingLevel?: CardHeadingLevel;
}
interface SurfaceOptions { variant?: SurfaceVariant; padded?: boolean }
interface SurfaceHandle { readonly element: HTMLElement; readonly options: Readonly<SurfaceOptions>; update(o: Partial<SurfaceOptions>): void; destroy(): void }

Pattern: HTML <article> named by its heading; interactive cards use the block-link pattern (Inclusive Components ‘Cards’): one real link on the title, stretched over the card with a pseudo-element.

Key Context Action
Tab / Shift+Tab Interactive card One tab stop: the title link. Other controls inside the card (for example footer buttons) are separate tab stops after it.
Enter Title link focused Follows the link (native).
Tab / Shift+Tab Non-interactive card The card itself is not focusable; focus moves through any controls in its content.
Element Role
titled card root article (native <article>)
untitled card root none (generic <div>)
title heading, level 2 to 6 (headingLevel, default 3)
interactive title link (native <a href>) inside the heading
Attribute Element Value
aria-labelledby <article> id of the title heading

Interactive cards draw the focus indicator on the whole card (:has(.aui-card__link:focus-visible)) and underline the title; the link’s own outline is suppressed so only one indicator shows.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.4 Link Purpose (In Context) A
2.4.7 Focus Visible AA
2.5.3 Label in Name A
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • Screen readers announce only the title as the link name, not the whole card content.
  • The media area is visually first but after the header in source order, so the heading is reached first when navigating by heading or reading.
  • Section containers are <div>s, not <header>/<footer>, so untitled cards never create banner or contentinfo landmarks.
  • Consumers must give informative images in media a text alternative, or alt=“” when decorative.
  • Mouse text selection inside interactive cards is limited by the link overlay.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.

default, raised, sunken, interactive, hover (interactive), focus-visible (interactive).

From Trigger To
untitled <div> update({ title }) <article>; content areas move to the new root
titled update({ title: undefined }) <div>; interactive must be removed first or in the same update
static update({ interactive: { href } }) block link
any global headingLevel change title heading re-created at the new level
  • Block-link pattern: wrapping the whole card in <a> would make screen readers read the entire card as the link name. Here only the title is the link, stretched with ::after.
  • The focus indicator is drawn around the whole card (:has(.aui-card__link:focus-visible)).
  • Media is visually first but comes after the header in source order, so heading navigation reaches the title first.
  • Give informative images a text alternative, or alt="" when decorative.
  • Manual screen-reader verification is pending.

Cards fill their container. For responsive collections, place them in an aui-grid aui-grid--auto (layout). Footer content wraps; media images are fluid.

Logical properties throughout; all sections follow the writing direction.

Colors, radius, shadow, and padding come from role tokens (space-inset follows density). Verified token pairs: color-text and color-text-muted on color-surface and color-surface-sunken, and color-link on color-surface. In forced-colors mode, the border stays, the title link uses LinkText, and focus uses a Highlight outline.

configureAbsoluteUI({ components: { card: { headingLevel: 2 } } });

Existing cards re-render their title at the new level.

The headingLevel option overrides global and theme defaults. All options are dynamic.

None. Cards render no icons of their own.

  • Title, subtitle, and string content use textContent. Node content is appended as-is; the consumer owns it.
  • interactive.href is checked with the WHATWG URL parser and must be http(s) or relative. javascript:, data:, and other schemes are rejected, including obfuscated forms such as java\tscript:.
  • No inline styles. CSP-safe.

No persistence, network access, or telemetry.

Constant DOM per card (root plus five section elements), no event listeners, and one configuration subscription. There is no dedicated performance budget yet.

Not data-driven. For long card collections, render only visible cards (virtualize) in the application.

  • interactive without title throws CARD_OPTIONS.
  • Adding or removing the title replaces card.element (the tag changes). body, footer, and media keep their content.
  • Controls inside an interactive card are lifted above the overlay (position: relative; z-index: 1), so they stay clickable.
  • update() without content keeps whatever the consumer appended to card.body.
Code Category Cause
CARD_OPTIONS configuration Blank title, invalid content types, invalid variant, interactive without a title, or an unsafe href.
SURFACE_OPTIONS configuration Invalid surface variant or padded.
SURFACE_ALREADY_APPLIED configuration Surface.apply on an element that already has an active surface handle.
CFG_INVALID_VALUE configuration headingLevel outside 2-6.

A failed construction leaves the host untouched and registers no subscription.

  • Mouse text selection inside interactive cards is limited by the link overlay.
  • The root element is replaced when the title is added or removed.
  • Manual screen-reader verification is pending.
  • HTML strings as title, subtitle, or content (pass a Node).
  • A button (non-navigation action) as the whole-card target.
  • Heading level 1 for card titles.