Card
The raised surface on the recessed page. Split it into parts with a header, body and footer, never by nesting cards.
Examples
Header, body and footer
Header and footer are separated by hairlines. The footer holds a hint on the left and buttons on the right.
Connection
How agents reach this server.
- Endpoint
- https://mcp.linear.app/mcp
- Auth
- OAuth
<Card>
<CardHeader
title="Connection"
description="How agents reach this server."
actions={<Button size="sm" variant="secondary" icon="refresh">Test</Button>}
/>
<CardBody>
<DescriptionList density="compact">…</DescriptionList>
</CardBody>
<CardFooter actions={<Button size="sm">Save</Button>}>
Last checked 2 minutes ago
</CardFooter>
</Card>Body only
The simplest card: a surface with padding.
Sentry has 18 tools. Agents see the 6 you enabled.
Sentry has 18 tools. Agents see the 6 you enabled.
<Card>
<CardBody>
<Text size="sm" tone="ink-2">Sentry has 18 tools. Agents see the 6 you enabled.</Text>
</CardBody>
</Card>Settings form
Wrap the card in a form so the footer button submits it. Header actions can be any control.
<form method="post" action={routes.servers.update.href({ id })}>
<Card>
<CardHeader
title="Notion"
description="Pages and databases in the Acme workspace."
actions={<Switch name="enabled" checked aria-label="Enabled" />}
/>
<CardFooter actions={<Button type="submit" size="sm">Save changes</Button>} />
</Card>
</form>Usage
import { Card, CardHeader, CardBody, CardFooter } from '@switchboard/ui'
<Card>
<CardHeader title="Tools" description="What agents can call on this server." />
<CardBody flush>
<DataTable … />
</CardBody>
</Card>Props
Every component also takes mix, for css() and mixins applied after its own style.
Card
| Prop | Type | Default | Description |
|---|---|---|---|
| as | 'section' | 'div' | 'article' | 'form' | 'li' | 'section' | Element to render. Attributes are typed as <section> ones, so wrap the card in a <form> when you need method and action. |
| mix | Mix | – | Extra css() or mixins, applied after the card style. |
Also accepts every attribute of the element it renders.
CardHeader
| Prop | Type | Default | Description |
|---|---|---|---|
| title | RemixNode | – | Card title, rendered as a heading. |
| description | RemixNode | – | One line under the title. |
| actions | RemixNode | – | Right-aligned controls: buttons, a switch, a menu. |
| level | 2 | 3 | 4 | 2 | Heading level of the title. |
Also accepts every <header> attribute. Children render under the description.
CardBody
| Prop | Type | Default | Description |
|---|---|---|---|
| flush | boolean | – | No padding, for tables and lists that run edge to edge. |
Also accepts every <div> attribute.
CardFooter
| Prop | Type | Default | Description |
|---|---|---|---|
| actions | RemixNode | – | Buttons, right-aligned. |
Also accepts every <footer> attribute. Children are the hint on the left.
Accessibility
- Card renders a <section> by default; a section with a heading inside is a named region only if you give it aria-labelledby. Use as="div" when it is just a surface.
- CardHeader renders a real heading (h2 by default). Set level so it fits the page outline.
- Inside a <form>, the footer submit button works without JavaScript.
Guidance
Do
- Put a DataTable in CardBody flush so the table runs to the card edges.
- Split a long card into header, body and footer parts.
Don’t
- Nest a Card inside a Card.
- Put more than one primary button in a card footer.
- Use a card to wrap a single line of page text; a paragraph will do.