Card
Material cards
Passive rich containers and native whole-card actions share sourced variants.
filled information
Rich passive content may contain its own structure and actions.
elevated information
Rich passive content may contain its own structure and actions.
outlined information
Rich passive content may contain its own structure and actions.
Passive card elements
div card
Passive card rendered as a native div.
section card
Passive card rendered as a native section.
import { useState } from 'react'
import {
Button,
Card,
Surface,
Text,
type CardVariant,
} from '@language-lit/material3-expressive'
const variants: readonly CardVariant[] = ['filled', 'elevated', 'outlined']
export function CardExample() {
const [openedCard, setOpenedCard] = useState('none')
return (
<Surface
as="section"
aria-labelledby="card-example-title"
color="surface-container-low"
shape="extra-large"
className="card-example"
>
<Text as="h2" id="card-example-title" variant="titleLarge" emphasis="emphasized">
Material cards
</Text>
<Text as="p" variant="bodyMedium">
Passive rich containers and native whole-card actions share sourced variants.
</Text>
<div className="card-example__grid">
{variants.map((variant) => (
<Card key={variant} variant={variant} className="card-example__item">
<Text as="h3" variant="titleMedium" emphasis="emphasized">
{variant} information
</Text>
<Text as="p" variant="bodyMedium">
Rich passive content may contain its own structure and actions.
</Text>
<Button variant="text" size="extra-small">
Card action
</Button>
</Card>
))}
</div>
<Text as="h3" variant="titleMedium" emphasis="emphasized">
Passive card elements
</Text>
<div className="card-example__grid">
<Card as="div" variant="filled" className="card-example__item">
<Text as="h4" variant="titleSmall" emphasis="emphasized">
div card
</Text>
<Text as="p" variant="bodyMedium">
Passive card rendered as a native div.
</Text>
</Card>
<Card as="section" variant="elevated" className="card-example__item">
<Text as="h4" variant="titleSmall" emphasis="emphasized">
section card
</Text>
<Text as="p" variant="bodyMedium">
Passive card rendered as a native section.
</Text>
</Card>
<Card as="aside" variant="outlined" className="card-example__item">
<Text as="h4" variant="titleSmall" emphasis="emphasized">
aside card
</Text>
<Text as="p" variant="bodyMedium">
Passive card rendered as a native aside.
</Text>
</Card>
</div>
<div className="card-example__grid">
{variants.map((variant) => (
<Card
key={variant}
interactive
variant={variant}
className="card-example__item"
onClick={() => setOpenedCard(variant)}
>
<Text as="span" variant="titleMedium" emphasis="emphasized">
Open {variant} card
</Text>
<Text as="small" variant="bodySmall">
Native button behavior
</Text>
</Card>
))}
{variants.map((variant) => (
<Card
key={`disabled-${variant}`}
interactive
disabled
variant={variant}
className="card-example__item"
>
Unavailable {variant} card
</Card>
))}
</div>
<Text as="span" role="status" variant="bodySmall" aria-live="polite">
Last opened: {openedCard}
</Text>
</Surface>
)
}Card groups one coherent subject using current Material filled, elevated, or
outlined treatment. Its discriminated API keeps rich passive containment and
whole-card native button behavior separate.
import { Button, Card } from '@language-lit/material3-expressive'
import '@language-lit/material3-expressive/styles.css'
<Card variant="filled">
<h2>Course progress</h2>
<p>Eight of twelve lessons complete.</p>
<Button variant="text">Continue</Button>
</Card>Contract
variantisfilled(default),elevated, oroutlined.- Passive mode is the default. It renders an
articleand acceptsas="div",section, orasidewhen those semantics better match the document. - Passive Card does not accept activation, focus, disabled, toggle, or form props. Its content may own headings, paragraphs, links, buttons, and other rich structure.
interactiverenders a native<button type="button">. Its button ref, submit/reset type, form owner, name/value, disabled state, ARIA/data attributes, and native handlers are preserved.- Card owns no content padding, typography, media layout, or named slots. Consumers compose those through children and normal class/style props.
Whole-card actions
Use interactive mode only when the entire card is one action:
<Card interactive variant="elevated" onClick={openLesson}>
<span>Open lesson</span>
<small>12 minutes</small>
</Card>A native HTML button may contain phrasing content only and cannot contain links, buttons, inputs, focusable descendants, or rich block structure. Development builds warn for unsafe intrinsic descendants that React can inspect. Custom components remain the consumer's responsibility because their rendered DOM is opaque to Card.
When a card needs headings, paragraphs, or nested actions, use passive Card and
place purpose-built controls inside it. Card deliberately does not use a
div role="button", an invisible overlay link, or synthetic keyboard handlers.
Link-card navigation and routing adapters are outside this task.
Variants and state
| Variant | Container | Rest / focus / press | Hover | Outline |
|---|---|---|---|---|
filled | surface-container-highest | Level 0 | Level 1 | none |
elevated | surface-container-low | Level 1 | Level 2 | none |
outlined | surface | Level 0 | Level 1 | outline-variant, 1px |
All variants use the medium corner and on-surface content. Outlined focus uses on-surface for its border. Interactive cards add Material hover, focus, and pressed state layers and reserve the theme's 48px minimum target. Disabled colors, outlines, and elevation follow the pinned first-party token files. Passive cards have no disabled state because they remain ordinary content.
Current AndroidX has no separate Expressive Card overload or Expressive card size/shape morph. This implementation therefore keeps current Card geometry and uses the theme's Expressive default-effects projection for container, border, and shadow transitions plus fast-effects for the state layer. Reduced motion makes those changes immediate.
Accessibility
The selected passive element owns document semantics; Card adds no role or tab
stop. Interactive mode relies on native button naming, pointer, Enter, Space,
focus, form, and disabled behavior. It does not expose aria-pressed, selected,
checked, or draggable state.
:focus-visible uses a token-backed secondary focus ring. Forced-colors mode
uses Canvas/CanvasText, Highlight focus, GrayText disabled treatment, and an
explicit border while removing translucent state layers and authored shadows.
Logical sizing and text-align: start follow RTL without DOM reordering.
Tokens and boundaries
Card registers searchable --m3e-comp-card-* variables for:
- minimum target, medium shape, focus ring, and disabled content;
- filled/elevated/outlined container and content colors;
- resting, hover, focus, pressed, and disabled shadows;
- outlined normal/hover/focus/pressed/disabled borders;
- variant-specific disabled container composition.
Theme overrides remain scoped to Material3Provider; Card injects no runtime
styles. It imports no Next.js, Vite, router, animation library,
or private application code.