Browse docs
Browse docs
Wrap content in a surface. Make it clickable. Show selection state.
import { Card, CardContent } from '@dashforge/tw';
<Card>
<CardContent>
<h3>Card title</h3>
<p>Card body.</p>
</CardContent>
</Card><Card variant="outlined">
<CardContent>Outlined surface.</CardContent>
</Card>
<Card variant="elevated">
<CardContent>Elevated surface (shadow).</CardContent>
</Card>
<Card variant="plain">
<CardContent>Plain surface (no border, no shadow).</CardContent>
</Card>import { Card, CardContent, Stack } from '@dashforge/tw';
<Stack direction="row" gap={3} wrap>
<Card variant="outlined">
<CardContent>Outlined — border, no shadow.</CardContent>
</Card>
<Card variant="elevated">
<CardContent>Elevated — shadow, no border.</CardContent>
</Card>
<Card variant="plain">
<CardContent>Plain — no border, no shadow.</CardContent>
</Card>
</Stack><CardContent> is a padded inner Box (p=4 default). Compose freely:
import { Card, CardContent, Button, Stack } from '@dashforge/tw';
<Card>
<CardContent>
<Stack gap={2}>
<div>
<div style={{ fontSize: 12, opacity: 0.6 }}>CUSTOMER</div>
<div style={{ fontSize: 18, fontWeight: 600 }}>ACME Inc.</div>
<div style={{ fontSize: 13, opacity: 0.7 }}>Enterprise plan</div>
</div>
<Stack direction="row" gap={2}>
<Button size="sm" variant="solid">View</Button>
<Button size="sm" variant="outline">Archive</Button>
</Stack>
</Stack>
</CardContent>
</Card>Wrap interactive content in <CardActionArea> to make the whole card a single clickable surface — with focus ring, hover state, and full keyboard support.
<Card>
<CardActionArea onClick={() => navigate('/customer/123')}>
<CardContent>
<h3>ACME Inc.</h3>
<p>Tap to view details.</p>
</CardContent>
</CardActionArea>
</Card><CardActionArea> uses Radix Slot — pass asChild to render onto another element (e.g. a router Link):
<Card>
<CardActionArea asChild>
<Link to="/customer/123">
<CardContent>
<h3>ACME Inc.</h3>
</CardContent>
</Link>
</CardActionArea>
</Card>Pass selected to toggle aria-pressed — useful for filter-card UX.
import { useState } from 'react';
import { Card, CardActionArea, CardContent, Stack } from '@dashforge/tw';
function PlanPicker() {
const [selected, setSelected] = useState('pro');
return (
<Stack direction="row" gap={3}>
{['starter', 'pro', 'enterprise'].map((plan) => (
<Card key={plan}>
<CardActionArea selected={selected === plan} onClick={() => setSelected(plan)}>
<CardContent>{plan}</CardContent>
</CardActionArea>
</Card>
))}
</Stack>
);
}<Card> is a thin opinionated <Box> alias — all Box props pass through. Override p, rounded, elevation, etc. directly on Card:
<Card p="lg" rounded="xl" elevation={3}>
<CardContent p="md">Heavily padded card with bigger radius + shadow.</CardContent>
</Card><Box> got access + visibleWhen in Sprint 4.4 — <Card> inherits both transparently. Use cases: admin-only revenue cards, role-gated dashboard tiles.
<Card access={{ requires: 'finance.view', when: 'denied:hide' }}>
<CardContent>
<h3>Monthly revenue</h3>
<p className="text-3xl font-bold">€48,250</p>
</CardContent>
</Card><Card visibleWhen={(engine) => engine.getValue('user.role') === 'admin'}>
<CardContent>Admin-only tile.</CardContent>
</Card>Configure <Card> defaults application-wide.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
Card: {
defaults: { variant: 'outlined', rounded: 'lg', elevation: 1 },
},
},
});Configurable axes (CardVariantProps):
| Axis | Type | Notes |
|---|---|---|
variant | 'outlined' | 'elevated' | 'plain' | Surface treatment. Narrower than Box (soft / solid excluded — those are Alert / banner territory). |
rounded | Box radius scale | Border radius identity — default 'lg'. |
elevation | 0 | 1 | 2 | 3 | 4 | 5 | Shadow scale — default 1. |
p | Box spacing step | Inner padding — default 0 (use <CardContent> for padded interior). |
Non-visual axes (Box access, visibleWhen, event handlers) are per-instance — not theme-configurable.
Note: theme.components.Box.defaults does not cascade into Card — they have independent theme entries. Setting theme.components.Card.defaults.variant = 'elevated' doesn't affect <Box>.
Precedence chain (lowest → highest):
variant: 'outlined', rounded: 'lg', elevation: 1, p: 0).theme.components.Card.defaults (application-wide).<Card variant="elevated" elevation={3} />) — wins over theme.sx — appended to the root and wins over conflicting classes via tailwind-merge.TypeScript: theme.components.Card.defaults autocompletes to the four axes above.
Reactivity: useComponentDefaults('Card') subscribes to the theme store — patchTheme re-renders every mounted instance inheriting the changed axis.
<Card> props<Card> is a thin alias of <Box> — all Box props apply (p, m, bg, rounded, elevation, access, visibleWhen, ...). The card-specific defaults:
| Default | Value |
|---|---|
variant | 'outlined' |
rounded | 'lg' |
elevation | 1 |
p | 0 |
The variant prop is narrowed to 'outlined' \| 'elevated' \| 'plain' (a subset of the full Box variant set).
| Prop | Type | Default | Description |
|---|---|---|---|
access | AccessRequirement | — | RBAC requirement — added in 1.1.0 (Sprint 4.4 surface alignment). Controls Box visibility / disabled-state via the centralized @dashforge/rbac policy engine.- onUnauthorized: 'hide' → Box does not render - onUnauthorized: 'disable' → adds aria-disabled + dimmed visual; interactive children should derive their disabled state from data-disabled on the Box or pass their own access - onUnauthorized: 'readonly' → adds aria-readonly; mostly used when Box wraps form sections that propagate readonly to childrenUse for permission-gated dashboard cards, admin-only sections, tenant-aware layouts. The hide path is the common case. |
as | ElementType | — | Override the rendered HTML tag. Defaults to 'div'. Useful when the surface should also carry semantic meaning — <Box as="section"> for a page section, <Box as="article"> for a card-shaped article.Ignored when asChild is true. |
asChild | boolean | — | Render via Radix Slot — the Box styles paint onto the single React child instead of wrapping it in our own element. Useful for <Box asChild><Link>...</Link></Box> to get a styled router link with no extra DOM wrapper.Mutually exclusive with as (when both are passed, asChild wins — see Box.tsx for the reasoning). |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'neutral' | Intent role. Applied by outlined / soft / solid; ignored by plain / elevated where color is not part of the surface identity. |
elevation | 0 | 1 | 2 | 3 | 4 | 5 | 0 | Shadow scale — primarily useful with variant='elevated'. 0 removes the shadow entirely. |
fullHeight | boolean | false | Stretch to fill the container's height (h-full). |
fullWidth | boolean | false | Stretch to fill the container's width (w-full). |
m | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Uniform margin — spacing token step. |
mx | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Horizontal margin — spacing token step. |
my | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Vertical margin — spacing token step. |
p | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Uniform padding — spacing token step. |
px | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Horizontal padding — spacing token step. |
py | 0 | 1 | 2 | 3 | 4 | '0.5' | 6 | 8 | 12 | 16 | 24 | — | Vertical padding — spacing token step. |
rounded | 'md' | 'sm' | 'lg' | 'none' | 'xl' | '2xl' | 'full' | 'none' | Border-radius identity. |
sx | string | — | Utility classes appended to the variant chain. Resolved via tailwind-merge so the consumer's classes always win over the variant defaults. Use for one-off overrides AND for utility dimensions Box deliberately doesn't expose as props (overflow, position, animation, etc.). |
variant | 'outlined' | 'elevated' | 'plain' | 'outlined' | Restricted variant — only the three card-appropriate Box variants. - 'outlined' (default) — bordered surface with subtle tint. - 'elevated' — surface with shadow, no border. - 'plain' — bare surface (background + radius), no border, no shadow. Use when wrapping content that already provides chrome. |
visibleWhen | (engine: Engine) => boolean | — | Reactive visibility predicate — added in 1.1.0 (Sprint 4.4 surface alignment). When the predicate returns false, the component renders null. Inside a <DashForm> it subscribes reactively to engine state changes; outside a form, it's evaluated as a plain closure.Use for state-driven hiding of dashboard cards, conditional sections, gated content, etc. For permission-driven hiding, prefer access with onUnauthorized: 'hide' — it's semantically the correct path and surfaces better in RBAC dev tools. |
<CardContent> propsPadded inner Box wrapper. Default p='md'. All Box props apply.
<CardActionArea> propsSlot-polymorphic clickable wrapper.
| Prop | Type | Default | Description |
|---|---|---|---|
onClick | () => void | — | Click handler. |
selected | boolean | false | Toggles aria-pressed. |
disabled | boolean | false | Disables clicks and focus. |
asChild | boolean | false | Render onto child element (Radix Slot). |
access | AccessSpec | — | RBAC gating. |
visibleWhen | (engine: Engine) => boolean | — | Engine-reactive predicate. |
className | string | — | Merged via tailwind-merge. |
<Card> doesn't expose a slotProps prop — it uses the compound sub-component pattern instead: you compose <Card> + <CardContent> + <CardActionArea> in JSX, styling each with its own sx / className prop.
| Sub-component | Role | How to style |
|---|---|---|
<Card> | Outer surface (variant + rounded + elevation) | sx or className on <Card>. |
<CardContent> | Padded inner section | p prop for padding step; sx for other classes. |
<CardActionArea> | Clickable wrapper for whole-card interactivity | sx / className on <CardActionArea>. |
This "explicit sub-component" pattern beats slotProps for Card because most consumers compose their own inner content anyway — the anatomy is fluid, not fixed. For fixed-anatomy compound components, see <DataGrid> or <Autocomplete>.
<Card> alone (with raw children) works fine for one-off layouts. <CardContent> is for the standard padded interior. <CardActionArea> is only when the whole card is interactive.access + visibleWhen come from Box (Sprint 4.4 retrofit). Same contract as the rest of the catalog.@dashforge/ui wraps @mui/material/Card directly. Same compositional pattern.