Skip to content

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.

React
tsx
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>;
dart
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

PropTypeDefaultDescription
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
elevationshared0 | 1 | 2 | 31Drop shadow depth. A card is a sheet lying on the page, so the default is 1
titleReactNodeThe card's heading. Pass a real heading element (title={<h2>…</h2>}) when it belongs in the document outline
subtitleReactNodeA second line under the title, one step down the type scale and muted
headerActionReactNodeContent pinned to the end of the header row — a menu button, a status chip. Stays on the title line while the title wraps
footerReactNodeThe bottom area. Laid out as a wrapping row, so a pair of buttons needs no wrapper
dividersbooleanfalseScores the sheet between sections instead of spacing them. The padding moves from the card onto each section so the rules reach both edges
paddedbooleantrueInner padding. Turn it off for full-bleed content — an image, a table
interactivebooleanfalseLifts the sheet under the pointer and adds a level of elevation. Give a genuinely clickable card a real element with render
renderuseRender.RenderPropRenders something other than a div — a <section>, an <li>, an <a href>
childrenReactNodeThe card's body
PropTypeDefaultDescription
variantsharedPlassVariantPlassVariant.glassWhat the sheet is made of. solid is the densest glass, glass is the default sheet with a hairline, ghost has no surface at all
sizesharedPlassSizePlassSize.mdThe radius, the type scale and the inner padding, moving together
colorsharedPlassColorPlassColor.primarySemantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensityPlassDensity.standardPadding only — never the height, never the type scale
elevationsharedint1Drop shadow depth. A card is a sheet lying on the page, so the default is 1
titleWidget?The card's heading. Pass a real heading element (title={<h2>…</h2>}) when it belongs in the document outline
subtitleWidget?A second line under the title, one step down the type scale and muted
headerActionWidget?Content pinned to the end of the header row — a menu button, a status chip. Stays on the title line while the title wraps
footerWidget?The bottom area. One widget, so a footer with two buttons in it brings its own Row or Wrap
dividersboolfalseScores the sheet between sections instead of spacing them. The padding moves from the card onto each section so the rules reach both edges
paddedbooltrueInner padding. Turn it off for full-bleed content — an image, a table
onPressedVoidCallback?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
interactiveboolfalseLifts the sheet under the pointer and adds a level of elevation. Give a genuinely clickable card a real element with render
semanticLabelString?The name a screen reader gives a pressable card. Left out, the card is named by what is in it
childWidget?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.

React

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.

React

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.

React

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.

React

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.

React

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.

React

Accessibility

  • Renders a plain <div> with no role, which is correct for a container. Use render to make it a <section>, an <li>, an <article> or a link when the markup should say more.
  • A plain string title is a styled <div>, not a heading. Pass title={<h2>…</h2>} when the card belongs in the document outline; it inherits the card's typography rather than the browser's.
  • interactive is a visual state. It adds no role, no tabIndex and no key handling. Give the card a real element with render instead of putting an onClick on a <div>.
  • The focus ring is drawn on :focus-visible and traces the sheet's own edge, so it only appears once the card is genuinely focusable.
  • A card with no onPressed adds no role and takes no focus stop, which is correct for a container.
  • title is styled as the title and is not announced as a heading. Wrap it in a Semantics(header: true, …) when the card belongs in the screen's outline; the typography is the card's either way.
  • interactive is a visual state. It adds no role, no focus stop and no key handling. Use onPressed when 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

ReactFlutterWhy
renderonPressedFlutter 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 footerone widgetThere 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.
childrenchildFlutter's name.
overflow-hidden beside padded={false}The sheet already clips to its own radius.
className, styleThere is no class list and no style attribute to pass through.

Released under the MIT License