Card

A surface that holds one block of a screen together: a head with its title and actions, and a body (also called a panel or tile). Reach for it for each block of a dashboard or a detail view.

import { Card, CardHeader, CardBody } from "@umriss-ui/core";

The line under the head is not the normal case; whitespace carries the hierarchy. Set it where the body runs edge to edge, a table or a log, together with a flush body.

A collapsible card turns its head into the button for its body, with the state in the card or, controlled, in your application.

Examples

Head and body

A CardHeader names the block, a CardBody holds whatever belongs to it; the card knows nothing of its content.

import { Card, CardBody, CardHeader, Text } from "@umriss-ui/core";

export default function HeadAndBody() {
  return (
    <Card style={{ maxWidth: 480 }}>
      <CardHeader title="Checkout" />
      <CardBody>
        <Text size="sm" tone="secondary">
          Payments team · tier 1 · 95th percentile below 300 ms, 99.95 % available a month.
        </Text>
      </CardBody>
    </Card>
  );
}

Eyebrow and actions

An eyebrow names the kind of thing above the title; actions stand on the right, a badge or a button or both.

import { Badge, Button, Card, CardBody, CardHeader, Stack, Text } from "@umriss-ui/core";

export default function EyebrowAndActions() {
  return (
    <Card style={{ maxWidth: 520 }}>
      <CardHeader
        eyebrow="Invoice"
        title="INV-26-0318 · Brandlow Office Supply"
        actions={
          <Stack direction="row" gap={2} align="center">
            <Badge tone="warning">Awaiting approval</Badge>
            <Button size="sm" variant="primary">
              Approve
            </Button>
          </Stack>
        }
      />
      <CardBody>
        <Text size="sm" tone="secondary">
          Facilities, CC-4400 · received 16 March, due 15 April · 2,321.98 € gross.
        </Text>
      </CardBody>
    </Card>
  );
}

A flush body

Where the body runs edge to edge, like a log or a table, set flush on the body and divider on the head.

import { Card, CardBody, CardHeader } from "@umriss-ui/core";

const LOG = `09:42:10  OPENED        INC-1048 by the latency alert on Checkout
09:46:02  ACKNOWLEDGED  Jonas Keller
09:58:37  NOTE          Card payments time out at the provider; retries doubled the load
10:11:05  MITIGATED     Retries capped at one, p95 back below 300 ms`;

export default function AFlushBody() {
  return (
    <Card style={{ maxWidth: 640 }}>
      <CardHeader divider eyebrow="INC-1048" title="Timeline" />
      <CardBody flush>
        <pre
          style={{
            margin: 0,
            padding: "var(--u-space-4)",
            overflowX: "auto",
            fontFamily: "var(--u-font-mono)",
            fontSize: "var(--u-text-xs)",
            lineHeight: 1.7,
            color: "var(--u-color-text-secondary)",
          }}
        >
          {LOG}
        </pre>
      </CardBody>
    </Card>
  );
}

Fold the body

Set collapsible and the head becomes the button for the body; defaultCollapsed starts it folded.

import { Badge, Card, CardBody, CardHeader, Stack, Text } from "@umriss-ui/core";

const LATE = [
  { stop: "Stop 4 · Marlow & Finch Books", window: "08:00–10:00", arrival: "10:35" },
  { stop: "Stop 9 · Ashcombe Dental", window: "10:00–12:00", arrival: "12:40" },
];

export default function FoldTheBody() {
  return (
    <Card collapsible defaultCollapsed style={{ maxWidth: 520 }}>
      <CardHeader eyebrow="Tour T-01" title="Late stops" actions={<Badge pill>{LATE.length}</Badge>} />
      <CardBody>
        <Stack gap={2}>
          {LATE.map((late) => (
            <Text key={late.stop} size="sm">
              {late.stop} - window {late.window}, planned {late.arrival}
            </Text>
          ))}
        </Stack>
      </CardBody>
    </Card>
  );
}

Fold from outside

Control collapsed with onCollapsedChange when a toolbar folds every card at once, or an address keeps which stand open.

import { useState } from "react";
import { Button, Card, CardBody, CardHeader, Stack, Text } from "@umriss-ui/core";

const PEOPLE = [
  { id: "arjun", name: "Arjun Mehta", work: "Member portal: sign-in form, 16 h left" },
  { id: "chloe", name: "Chloe Durand", work: "Member portal: profile page, 10 h left" },
  { id: "noah", name: "Noah Fischer", work: "Online shop: search results design, 6 h left" },
];

export default function FoldFromOutside() {
  const [folded, setFolded] = useState<ReadonlySet<string>>(() => new Set(["noah"]));
  const fold = (id: string, collapsed: boolean) =>
    setFolded((previous) => {
      const next = new Set(previous);
      if (collapsed) next.add(id);
      else next.delete(id);
      return next;
    });

  return (
    <Stack gap={3} style={{ maxWidth: 520 }}>
      <Stack direction="row" gap={2}>
        <Button size="sm" onClick={() => setFolded(new Set())}>
          Open all
        </Button>
        <Button size="sm" onClick={() => setFolded(new Set(PEOPLE.map((p) => p.id)))}>
          Fold all
        </Button>
      </Stack>
      {PEOPLE.map((person) => (
        <Card
          key={person.id}
          collapsible
          collapsed={folded.has(person.id)}
          onCollapsedChange={(collapsed) => fold(person.id, collapsed)}
        >
          <CardHeader eyebrow="Sprint 14" title={person.name} />
          <CardBody>
            <Text size="sm">{person.work}</Text>
          </CardBody>
        </Card>
      ))}
    </Stack>
  );
}

Cards on a dashboard

Each block of a dashboard is a card in a grid; a card with nothing to show says so with an EmptyState.

import { Badge, Button, Card, CardBody, CardHeader, EmptyState, Grid, Stack, Text } from "@umriss-ui/core";

const INCIDENTS = [
  { id: "INC-1048", title: "Checkout slow, card payments time out", severity: "SEV1" },
  { id: "INC-1047", title: "Webhook deliveries delayed", severity: "SEV3" },
];

export default function CardsOnADashboard() {
  return (
    <Grid minItemWidth="240px" gap={4}>
      <Card>
        <CardHeader title="Open incidents" actions={<Badge tone="danger">{INCIDENTS.length}</Badge>} />
        <CardBody>
          <Stack gap={3}>
            {INCIDENTS.map((incident) => (
              <Stack key={incident.id} gap={1}>
                <Text size="xs" tone="muted">
                  {incident.id} · {incident.severity}
                </Text>
                <Text size="sm">{incident.title}</Text>
              </Stack>
            ))}
          </Stack>
        </CardBody>
      </Card>
      <Card>
        <CardHeader title="On call" />
        <CardBody>
          <Stack gap={2}>
            <Text size="sm">Primary: Jonas Keller</Text>
            <Text size="sm">Secondary: Ada Mwangi</Text>
            <Text size="xs" tone="muted">
              Handover tomorrow at 09:00
            </Text>
          </Stack>
        </CardBody>
      </Card>
      <Card>
        <CardHeader title="Snoozed alerts" />
        <CardBody>
          <EmptyState
            title="Nothing snoozed"
            description="Alerts you snooze wait here until they wake."
            action={<Button size="sm">Open the alert list</Button>}
          />
        </CardBody>
      </Card>
    </Grid>
  );
}

When to use something else

Keyboard

Key Action
Tab Reaches the actions in the head and, in a collapsible card, its Show or Hide button; a folded body is inert and passed over.
Enter / Space On Show or Hide, unfolds or folds the body.

The keys of Button apply here.

API

CardProps

NameTypeDefaultDescription
collapsiblebooleanfalseAllows collapsing and expanding the body through the header.

Shown in: Fold the body, Fold from outside

defaultCollapsedbooleanfalseInitial state of the collapse mechanism; afterwards it belongs to the card. Only effective together with collapsible.

Shown in: Fold the body

collapsedboolean—Controlled: whether the body is folded away - for a card folded from outside, or one whose state is kept in an address. Only effective together with collapsible.

Shown in: Fold from outside

onCollapsedChange(collapsed: boolean) => void—Reports the wish of the header's button. Controlled, the card folds only once collapsed follows; uncontrolled it is merely a message.

Shown in: Fold from outside

refRef<HTMLElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Also takes every attribute of the rendered element.

CardHeaderProps

NameTypeDefaultDescription
titlerequiredReactNode—The heading of the card. A node and not merely text, so that an identifier with markup can stand in it too.

Shown in: Head and body, Eyebrow and actions, A flush body and 27 more

eyebrowReactNode—Small all-caps label above the title, e.g. a category.

Shown in: Eyebrow and actions, A flush body, Fold the body and 17 more

actionsReactNode—Right-aligned area, e.g. buttons or badges.

Shown in: Eyebrow and actions, Fold the body, Cards on a dashboard and 13 more

dividerbooleanfalseA dividing line below the head. Only set it where the body needs it - typically with flush data (flush tables, logs). With normal content the white space carries the hierarchy on its own.

Shown in: A flush body

refRef<HTMLDivElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Also takes every attribute of <div> – without title.

CardBodyProps

NameTypeDefaultDescription
flushbooleanfalseRemoves the inner padding, e.g. for flush tables.

Shown in: A flush body

refRef<HTMLDivElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Also takes every attribute of <div>.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index