PlCard
The sheet everything else on a screen is grouped onto, with the parts a card is made of already laid out on it: a title, a subtitle, a body and a footer.
import { PlButton, PlCard } from 'plass-ui';
<PlCard title="Team plan" subtitle="Billed yearly" footer={<PlButton>Upgrade</PlButton>}>
Shared projects, audit logs and a seat for anyone you invite.
</PlCard>;import 'package:plass_ui/plass_ui.dart';
PlCard(
title: const Text('Team plan'),
subtitle: const Text('Billed yearly'),
footer: PlButton(onPressed: upgrade, child: const Text('Upgrade')),
child: const Text('Shared projects, audit logs and a seat for anyone you invite.'),
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'glass' | 'ghost' | 'glass' | What the sheet is made of. solid is the densest glass, glass is the default sheet with a hairline, ghost has no surface at all |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The radius, the type scale and the inner padding, moving together |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 1 | Drop shadow depth. A card is a sheet lying on the page, so the default is 1 |
| title | ReactNode | — | The card's heading. Pass a real heading element (title={<h2>…</h2>}) when it belongs in the document outline |
| subtitle | ReactNode | — | A second line under the title, one step down the type scale and muted |
| headerAction | ReactNode | — | Content pinned to the end of the header row — a menu button, a status chip. Stays on the title line while the title wraps |
| footer | ReactNode | — | The bottom area. Laid out as a wrapping row, so a pair of buttons needs no wrapper |
| dividers | boolean | false | Scores the sheet between sections instead of spacing them. The padding moves from the card onto each section so the rules reach both edges |
| padded | boolean | true | Inner padding. Turn it off for full-bleed content — an image, a table |
| interactive | boolean | false | Lifts the sheet under the pointer and adds a level of elevation. Give a genuinely clickable card a real element with render |
| render | useRender.RenderProp | — | Renders something other than a div — a <section>, an <li>, an <a href> |
| children | ReactNode | — | The card's body |
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | PlassVariant | PlassVariant.glass | What the sheet is made of. solid is the densest glass, glass is the default sheet with a hairline, ghost has no surface at all |
| sizeshared | PlassSize | PlassSize.md | The radius, the type scale and the inner padding, moving together |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity | PlassDensity.standard | Padding only — never the height, never the type scale |
| elevationshared | int | 1 | Drop shadow depth. A card is a sheet lying on the page, so the default is 1 |
| title | Widget? | — | The card's heading. Pass a real heading element (title={<h2>…</h2>}) when it belongs in the document outline |
| subtitle | Widget? | — | A second line under the title, one step down the type scale and muted |
| headerAction | Widget? | — | Content pinned to the end of the header row — a menu button, a status chip. Stays on the title line while the title wraps |
| footer | Widget? | — | The bottom area. One widget, so a footer with two buttons in it brings its own Row or Wrap |
| dividers | bool | false | Scores the sheet between sections instead of spacing them. The padding moves from the card onto each section so the rules reach both edges |
| padded | bool | true | Inner padding. Turn it off for full-bleed content — an image, a table |
| onPressed | VoidCallback? | — | Called when pressed. Passing it makes the card a real focus stop, announced as a button — the difference between a card that looks clickable and one that is |
| interactive | bool | false | Lifts the sheet under the pointer and adds a level of elevation. Give a genuinely clickable card a real element with render |
| semanticLabel | String? | — | The name a screen reader gives a pressable card. Left out, the card is named by what is in it |
| child | Widget? | — | The card's body |
Every native <div> attribute passes straight through. color and title are excluded because both are Plass props here.
footer is one widget, so a footer with two buttons in it brings its own Row or Wrap. The React build lays out a fragment of children for you; there is no fragment here to lay out.
What the shared axes (variant size color density elevation) mean across the library is in prop conventions.
Examples
variant
The three materials, read the way a container reads them. solid is the clear glass at its most opaque, for a panel that has to sit forward of everything around it. glass is the canonical Plass sheet and the default. ghost has no sheet at all, for a card inside a card, where a second bordered rectangle is a second rectangle.
None of the three is dyed. What a card holds arrives with its own colours, and tinting the sheet under them would put every one on a background it was not chosen against.
title · subtitle · headerAction · footer
The sections are props rather than sub-components, for the same reason PlTextField takes label and description as props: the arrangement is fixed, and what a caller decides is what goes in each slot.
A slot that is empty draws nothing. A card with only a body is one section, not three.
dividers
Off by default: a card's sections are told apart by space. Turn it on and they are scored with a hairline in --plass-divider, the neutral ink a PlList and a PlTable are ruled with, so every internal rule in the library is one line. It is not the sheet's own white edge, which reads only because the page wash is behind it and would have the pane behind it here. The rules have to reach both edges, so the padding moves from the card onto each section.
padded
Off, the sheet keeps no inset at all and the content brings its own, a banner image reaching all four corners, a table drawing its own rows.
Pair it with overflow-hidden so the content is clipped to the card's radius.
The sheet already clips to its own radius, so a banner reaching the edges is rounded without being asked.
interactiveonPressed and interactive
Lifts the sheet under the pointer and puts a level of elevation under it. This is the one place the library allows a surface to move, and the exception is the rule rather than a hole in it: what may not move is the thing under the finger. A sheet that holds content is the other kind of surface, and lifting one is how a pane of glass says it can be picked up.
interactive changes how the card looks and nothing else. A card that is genuinely clickable has to be a real element, render={<a href="…" />} or render={<button type="button" />}, so it is focusable, announced as what it is, and reachable from a keyboard.
onPressed is the one to use: it makes the card a real focus stop, announced as a button, activated by Enter or Space, and it lifts. interactive is the same lift without any of that, for a card whose interactive parts are the widgets inside it.
size
Moves the radius, the type scale and the inner padding together. Unlike a control, size on a card does not set a height: a card is as tall as what it holds.
Accessibility
- Renders a plain
<div>with no role, which is correct for a container. Userenderto make it a<section>, an<li>, an<article>or a link when the markup should say more. - A plain string
titleis a styled<div>, not a heading. Passtitle={<h2>…</h2>}when the card belongs in the document outline; it inherits the card's typography rather than the browser's. interactiveis a visual state. It adds no role, notabIndexand no key handling. Give the card a real element withrenderinstead of putting anonClickon a<div>.- The focus ring is drawn on
:focus-visibleand traces the sheet's own edge, so it only appears once the card is genuinely focusable.
- A card with no
onPressedadds no role and takes no focus stop, which is correct for a container. titleis styled as the title and is not announced as a heading. Wrap it in aSemantics(header: true, …)when the card belongs in the screen's outline; the typography is the card's either way.interactiveis a visual state. It adds no role, no focus stop and no key handling. UseonPressedwhen the card is genuinely something you press.- The focus ring only appears on what CSS calls
:focus-visible(a keyboard reaching the card, never a pointer clicking it), and traces the sheet's own edge.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
render | onPressed | Flutter has no polymorphic element, and the thing render was mostly used for, making the card real, is what onPressed does directly. An action that navigates calls your router from it. |
a fragment in footer | one widget | There is no fragment to lay out, so a footer with several things in it brings its own Row or Wrap. |
title={<h2>…</h2>} | Semantics(header: true, …) | Flutter's semantics tree has one heading flag and no depth to go with it. |
children | child | Flutter's name. |
overflow-hidden beside padded={false} | — | The sheet already clips to its own radius. |
className, style | — | There is no class list and no style attribute to pass through. |