Card is a bounded surface. Use it when the content is a distinct module inside
a larger page: contact summaries, custom field groups, analytics panels, stats,
previews, dashboards, and grouped settings blocks.
Do not use Card as the default wrapper for every form or page section. Most
page forms, wizard steps, and full-height page content should use plain layout
containers so the page does not become a stack of artificial boxes.
Card and Widget look similar but solve different jobs. Widget composes Card under the hood and adds dashboard-specific slots — pick by the job, not by the visual.
| You're building… | Use | Why |
|---|---|---|
| A KPI tile, chart tile, paginated table tile, or any block that lives in a dashboard grid | Widget | Widget bakes in title + filters + actions + footer + sortable drag handle. Don't hand-roll these on top of Card. |
| A bounded summary surface that is not part of a dashboard — contact profile, file preview, account-detail panel, settings detail block | Card | Card is the primitive. Compose CardHeader + CardContent directly. |
| A full-page section, a wizard step, a form group, a filter toolbar, or any block that spans the page | Plain <div> / <section> | Neither Card nor Widget — those are for bounded modules, not page chrome. |
Quick decision flow:
Widget.Card.Widget composes Card, so anything you can put in CardContent you can put in Widget. The reverse is not true — Widget adds drag, pagination footer, filter rows, and the dashboard-aligned header layout.
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from '@marmoui/ui';Stats cards use compact 16px padding (`px-space-md py-space-md`), different from normal content cards.
Sent
13,796
Opened
2,197 - 15.92%
Unique Clicks
49 - (29 unique) - 0.21%
Click Conversions
4 - (2%)
Ecommerce Conversions
37 | $4.4
Unsubscribed
7 - 0.05%
const stats = [
{ label: 'Sent', value: '13,796' },
{ label: 'Opened', value: '2,197 - 15.92%' },
{ label: 'Unique Clicks', value: '49 - (29 unique) - 0.21%' },
{ label: 'Click Conversions', value: '4 - (2%)' },
{ label: 'Ecommerce Conversions', value: '37 | $4.4' },
{ label: 'Unsubscribed', value: '7 - 0.05%' },
];
<div className="grid gap-space-md md:grid-cols-3">
{stats.map(stat => (
<Card key={stat.label}>
<CardContent className="px-space-md py-space-md">
<p className="text-caps-sm font-medium uppercase tracking-caps-sm text-ink-light">
{stat.label}
</p>
<p className="mt-space-2xs text-heading-md font-medium text-ink-dark">{stat.value}</p>
</CardContent>
</Card>
))}
</div>A card can group identity, labels, and contact details without becoming a page-action container.
johndoe@company.com
function ContactDetail({ icon, label, value, emphasize = false }) {
return (
<div className="flex items-center justify-between gap-space-md">
<dt className="flex min-w-0 items-center gap-space-xs text-ink">
<span className="text-icon">{icon}</span>
<span className="truncate">{label}</span>
</dt>
<dd className={emphasize ? 'font-medium text-primary-solid' : 'text-ink-light'}>{value}</dd>
</div>
);
}
<Card className="max-w-md overflow-hidden">
<CardHeader>
<div className="flex justify-between gap-4">
<Avatar size="lg">
<AvatarImage src="https://api.dicebear.com/9.x/lorelei/svg" alt="User Avatar" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-2 items-start">
<div className="flex gap-1 items-center">
<CardTitle className="text-heading-md font-medium text-ink-dark">John Doe</CardTitle>
<CardDescription className="text-ink-light">johndoe@company.com</CardDescription>
</div>
<Badge variant="success">Repeated Customers 120</Badge>
<ButtonGroup attached={false} size="sm">
<IconButton variant="secondary" icon={<MdSend />} aria-label="Send Email" />
<IconButton variant="secondary" icon={<MdMoreHoriz />} aria-label="More actions" />
</ButtonGroup>
</div>
</div>
</CardHeader>
<CardContent>
<dl className="grid gap-space-sm text-sm">
<ContactDetail icon={<MdLocationOn />} label="Location" value="California, USA" />
<ContactDetail icon={<MdPhoneIphone />} label="Phone" value="-" />
<ContactDetail icon={<MdBusiness />} label="Company" value="Acme Inc." />
<ContactDetail icon={<MdTableRows />} label="Lists" value="4" emphasize />
<ContactDetail icon={<MdShoppingCart />} label="E-Commerce" value="2" emphasize />
</dl>
</CardContent>
</Card>Use CardHeader for titled card sections, similar to dialog headers: present when it anchors the card content.
const fields = [
['Package', '-'],
['Address', 'Los Angeles, CA'],
['Shipping Address', 'San Francisco, CA'],
['Contact Number', 'N/A'],
['Business Name', 'TechCorp'],
['Total Spend', '$50.00'],
['Customer Segments', '5'],
];
<Card className="max-w-md overflow-hidden">
<CardHeader>
<CardTitle>Contact Fields</CardTitle>
</CardHeader>
<CardContent>
<dl className="grid gap-space-sm text-sm">
{fields.map(([label, value]) => (
<div key={label} className="flex justify-between gap-space-md">
<dt className="text-ink">{label}</dt>
<dd className="text-right text-ink-light">{value}</dd>
</div>
))}
</dl>
</CardContent>
</Card>Place controls in CardHeader only when they change, filter, configure, or refresh this card's own content.
<Card className="max-w-xl overflow-hidden">
<CardHeader className="border-b border-border-secondary p-space-lg">
<div className="flex items-center justify-between gap-space-md">
<CardTitle>Lists History</CardTitle>
<IconButton className="-mt-1" variant="ghost" size="sm" aria-label="Configure chart" icon={<MdSettings />} />
</div>
<div className="flex flex-wrap gap-space-xs pt-space-sm">
<Select defaultValue="all">
<SelectTrigger className="w-40">
<SelectValue placeholder="Select list" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Lists</SelectItem>
<SelectItem value="customers">Customers</SelectItem>
<SelectItem value="leads">Leads</SelectItem>
</SelectContent>
</Select>
<DatePicker placeholder="Select date range" isRange triggerType="input" />
</div>
</CardHeader>
<CardContent>
<div className="flex h-20 items-center justify-center">Dynamic content goes here</div>
</CardContent>
</Card>Forms do not need a Card unless the form itself is a widget inside a larger surface.
<form className="mx-auto w-full max-w-2xl space-y-4">
<Field label="First name" required>
<Input placeholder="Ava" required />
</Field>
<Field label="Email" required>
<Input type="email" placeholder="ava@example.com" required />
</Field>
<div className="flex justify-end gap-2">
<Button variant="secondary" type="button">Cancel</Button>
<Button type="submit">Save contact</Button>
</div>
</form>| You want… | Use this |
|---|---|
| A KPI tile, chart tile, paginated table tile, or any reorderable dashboard cell | Widget |
| A contact summary, account summary, file/media preview, or other non-dashboard bounded surface | Card with normal section padding |
| A selectable option with card visuals | RadioCard or CheckboxCard |
| A full page with header/actions | PageSection + plain page layout |
| A form page or wizard step | Plain layout container, not Card by default |
| A table page | DataTable inside the page layout |
For dashboard-tile examples — Referral Sources, Automation, Leads Funnel, Lists Growth, Chatbot Stats, Top Buyers — see Widget. Widget composes Card internally and bakes in the filter / actions / footer / sortable slots so you don't hand-roll them on top of Card.
Card + CardHeader + filter row for a dashboard tile. Use Widget — it composes Card and ships the filter / actions / footer / sortable slots.Card only when the form is a bounded widget inside a larger page.px-space-md py-space-md (16px on all sides).Panel, PageSection, and plain flex/grid containers for page structure.shadow-card utility for card surfaces.space-y-*, gap-*, px-*, and py-* instead.