component
A versatile card component for grouping related content. Card uses the Panel core component as its surface layer, supporting the same theming system — feedback colors, background styles, border, and radius control — while adding a structured layout with optional header (heading, subheading, leading, trailing), a content area, and an optional footer.
Changelog · v2.4
June 3, 2026 · 4 min read
Every color, space, and type value now flows from a single source of truth — tweak one token and the change ripples through every component, in every framework, automatically. This release also ships fluid headings and the new numeric text styles.
---
import Card from "../Card.astro";
import Avatar from "@pindoba/astro-avatar";
import Badge from "@pindoba/astro-badge";
import Button from "@pindoba/astro-button";
import Stamp from "@pindoba/astro-stamp";
import { ArrowRight, Sparkles } from "@lucide/astro";
import { css } from "@pindoba/styled-system/css";
import { flex } from "@pindoba/styled-system/patterns";
---
<div class={css({ width: "100%", maxWidth: "42rem" })}>
<Card
border="default"
shadow="sm"
radius="lg"
header={{
eyebrow: "Changelog · v2.4",
heading: "Design tokens, refreshed",
subheading: "June 3, 2026 · 4 min read",
layout: {
leading: { span: "below" },
trailing: { align: "start" },
},
}}
>
<Stamp
slot="header-leading"
size="md"
shape="square"
feedback="primary"
emphasis="primary"
>
<Sparkles />
</Stamp>
<Badge slot="header-trailing" feedback="success">New</Badge>
<p class={css({ margin: "0", color: "panel.text" })}>
Every color, space, and type value now flows from a single source of truth
— tweak one token and the change ripples through every component, in every
framework, automatically. This release also ships fluid headings and the
new numeric text styles.
</p>
<div slot="footer-leading" class={flex({ gap: "xs", align: "center" })}>
<Avatar
name="Helena Rocha"
src="https://i.pravatar.cc/96?img=32"
size="sm"
/>
<div class={css({ display: "flex", flexDirection: "column" })}>
<span
class={css({
fontSize: "sm",
fontWeight: "semibold",
color: "panel.text.bold",
lineHeight: "tight",
})}>Helena Rocha</span
>
<span
class={css({
fontSize: "xs",
color: "panel.text.muted",
lineHeight: "tight",
})}>Design Systems</span
>
</div>
</div>
<Button slot="footer-trailing" size="sm">
Read more
<ArrowRight />
</Button>
</Card>
</div>Cards support semantic feedback colors. The heading, subheading, border, and background all follow the selected color palette.
12 updates since Monday
A neutral card sets the calm default — quiet enough for routine notifications that don't demand attention.
Now available in Settings
Primary leans on the brand color to spotlight announcements, new features, and the action you'd most like someone to take.
Invoice #4,128 · R$ 420.00
Success confirms something went right — a completed payment, a passing build, a saved change.
8.7 GB of 10 GB used
Warning flags something that needs attention soon but isn't broken yet — a soft nudge before it becomes a problem.
Renew to restore access
Danger marks the highest-stakes states — failures, expirations, and destructive actions that can't be undone.
---
import Card from "../Card.astro";
import Button from "@pindoba/astro-button";
import Stamp from "@pindoba/astro-stamp";
import {
Bell,
CircleCheck,
Megaphone,
OctagonAlert,
TriangleAlert,
} from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
const items = [
{
feedback: "neutral",
icon: Bell,
heading: "Weekly digest is ready",
subheading: "12 updates since Monday",
body: "A neutral card sets the calm default — quiet enough for routine notifications that don't demand attention.",
cta: "View all",
},
{
feedback: "primary",
icon: Megaphone,
heading: "Dark mode has landed",
subheading: "Now available in Settings",
body: "Primary leans on the brand color to spotlight announcements, new features, and the action you'd most like someone to take.",
cta: "Try it now",
},
{
feedback: "success",
icon: CircleCheck,
heading: "Payment received",
subheading: "Invoice #4,128 · R$ 420.00",
body: "Success confirms something went right — a completed payment, a passing build, a saved change.",
cta: "View receipt",
},
{
feedback: "warning",
icon: TriangleAlert,
heading: "Storage almost full",
subheading: "8.7 GB of 10 GB used",
body: "Warning flags something that needs attention soon but isn't broken yet — a soft nudge before it becomes a problem.",
cta: "Upgrade plan",
},
{
feedback: "danger",
icon: OctagonAlert,
heading: "Subscription expired",
subheading: "Renew to restore access",
body: "Danger marks the highest-stakes states — failures, expirations, and destructive actions that can't be undone.",
cta: "Renew now",
},
] as const;
---
<div class={stack({ gap: "md", direction: "column", width: "100%" })}>
{
items.map((item) => {
const LeadingIcon = item.icon;
return (
<Card
feedback={item.feedback}
border="default"
header={{
heading: item.heading,
subheading: item.subheading,
}}
>
<Stamp
slot="header-leading"
size="md"
shape="square"
feedback={item.feedback}
emphasis="secondary"
>
<LeadingIcon />
</Stamp>
<p class={css({ margin: "0", color: "panel.text" })}>{item.body}</p>
<Button slot="footer-trailing" size="sm" feedback={item.feedback}>
{item.cta}
</Button>
</Card>
);
})
}
</div>The size prop controls the padding of the header, content, and footer sections, as well as the heading font size.
staging → main · 14 commits
Roll out the latest build to every region. Approvals are in and the canary has stayed healthy for 20 minutes.
staging → main · 14 commits
Roll out the latest build to every region. Approvals are in and the canary has stayed healthy for 20 minutes.
staging → main · 14 commits
Roll out the latest build to every region. Approvals are in and the canary has stayed healthy for 20 minutes.
staging → main · 14 commits
Roll out the latest build to every region. Approvals are in and the canary has stayed healthy for 20 minutes.
staging → main · 14 commits
Roll out the latest build to every region. Approvals are in and the canary has stayed healthy for 20 minutes.
---
import Card from "../Card.astro";
import Badge from "@pindoba/astro-badge";
import Button from "@pindoba/astro-button";
import Stamp from "@pindoba/astro-stamp";
import { Rocket } from "@lucide/astro";
import { stack, flex } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
const sizes = [
{ size: "2xs", label: "2X-Small" },
{ size: "xs", label: "X-Small" },
{ size: "sm", label: "Small" },
{ size: "md", label: "Medium · default" },
{ size: "lg", label: "Large" },
] as const;
---
<div class={stack({ gap: "xl", direction: "column", width: "100%" })}>
{
sizes.map((s) => (
<div>
<span
class={css({
textStyle: "label.sm",
color: "panel.text.muted",
display: "block",
marginBottom: "xs",
})}
>
{s.label}
</span>
<Card
size={s.size}
border="default"
shadow="sm"
header={{
heading: "Deploy to production",
subheading: "staging → main · 14 commits",
}}
>
<Stamp
slot="header-leading"
size="md"
shape="square"
feedback="primary"
emphasis="primary"
>
<Rocket />
</Stamp>
<Badge slot="header-trailing" feedback="warning">
Queued
</Badge>
<p class={css({ margin: "0", color: "panel.text" })}>
Roll out the latest build to every region. Approvals are in and the
canary has stayed healthy for 20 minutes.
</p>
<span
slot="footer-leading"
class={css({ fontSize: "xs", color: "panel.text.muted" })}
>
Queued 2 min ago
</span>
<div
slot="footer-trailing"
class={flex({ gap: "sm", align: "center" })}
>
<Button emphasis="secondary" size="sm">
Cancel
</Button>
<Button feedback="primary" size="sm">
Deploy
</Button>
</div>
</Card>
</div>
))
}
</div>The card’s header and footer slots accept rich content. Place an icon in the leading position to help users identify the card’s category at a glance, add a ghost button to trailing for contextual actions like settings or a menu, and use the footer trailing for primary and secondary action buttons.
Edited 2 hours ago by Felipe
Outlines the quarter's milestones, owners, and target ship dates across the platform and design-system tracks.
Visibility, access, and integrations
Control who can view and edit this project, and connect the tools your team already uses every day.
Product designer · São Paulo
Update the personal details your teammates see across the workspace.
---
import Card from "../Card.astro";
import Button from "@pindoba/astro-button";
import Badge from "@pindoba/astro-badge";
import {
CircleCheck,
Clock,
Ellipsis,
FileText,
FlaskConical,
Pencil,
Settings,
User,
} from "@lucide/astro";
import { stack, flex } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
const labelClass = css({
textStyle: "label.sm",
color: "panel.text.muted",
display: "block",
marginBottom: "xs",
});
// Top-align the header trailing slot (badges / buttons) against the
// multi-line heading + subheading block.
const topTrailing = { layout: { trailing: { align: "start" } } } as const;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<span class={labelClass}>Icon in the header</span>
<Card
header={{
heading: "Q3 Roadmap.pdf",
subheading: "Edited 2 hours ago by Felipe",
...topTrailing,
}}
>
<FileText size={20} slot="heading-leading" />
<Clock size={14} slot="subheading-leading" />
<Badge feedback="warning" slot="header-trailing">
<Pencil class={css({ w: "1em", h: "1em" })} />
Draft
</Badge>
<p class={css({ margin: "0", color: "panel.text" })}>
Outlines the quarter's milestones, owners, and target ship dates across
the platform and design-system tracks.
</p>
</Card>
</div>
<div>
<span class={labelClass}>Ghost button in the header</span>
<Card
header={{
heading: "Project settings",
subheading: "Visibility, access, and integrations",
...topTrailing,
}}
>
<Settings size={20} slot="header-leading" />
<div class={flex({ gap: "sm", align: "center" })} slot="header-trailing">
<Badge feedback="primary" emphasis="secondary">
<FlaskConical class={css({ w: "1em", h: "1em" })} />
Beta
</Badge>
<Button emphasis="ghost" shape="circle">
<Ellipsis />
</Button>
</div>
<p class={css({ margin: "0", color: "panel.text" })}>
Control who can view and edit this project, and connect the tools your
team already uses every day.
</p>
</Card>
</div>
<div>
<span class={labelClass}>Actions in the footer</span>
<Card
header={{
heading: "Helena Rocha",
subheading: "Product designer · São Paulo",
...topTrailing,
}}
>
<User size={20} slot="heading-leading" />
<Badge feedback="success" slot="header-trailing">
<CircleCheck class={css({ w: "1em", h: "1em" })} />
Active
</Badge>
<p class={css({ margin: "0", color: "panel.text" })}>
Update the personal details your teammates see across the workspace.
</p>
<div class={flex({ gap: "sm" })} slot="footer-trailing">
<Button emphasis="secondary">Cancel</Button>
<Button>Save changes</Button>
</div>
</Card>
</div>
</div>When the header has a trailing element, the card pins it to the top-right by default (layout.trailing.align: "start"), so contextual actions align with the first line of a multi-line heading instead of centering. Override it per card via header={{ layout: { trailing: { align: "center" } } }}, and use header={{ layout: { trailing: { justify: "end" } } }} to flush a fixed-width trailing region (like a metadata path) to the right edge.
selected renders a dashed border to mark the active card (visible when the card also has a border), and muted dims and desaturates a locked or unavailable card. Both are purely visual — no more reaching for passThrough.root.style with borderStyle / opacity / filter.
The card in its resting state.
selected renders a dashed border to mark the active card.
muted dims and desaturates a locked or unavailable card.
---
import Card from "../Card.astro";
import { css } from "@pindoba/styled-system/css";
import { grid } from "@pindoba/styled-system/patterns";
const labelClass = css({
textStyle: "label.sm",
color: "panel.text.muted",
display: "block",
marginBottom: "xs",
});
---
<div class={grid({ columns: { base: 1, sm: 3 }, gap: "lg" })}>
<div>
<span class={labelClass}>Default</span>
<Card border="default" header={{ heading: "Main branch" }}>
<p class={css({ margin: "0", color: "panel.text" })}>
The card in its resting state.
</p>
</Card>
</div>
<div>
<span class={labelClass}>Selected</span>
<Card selected border="default" header={{ heading: "Main branch" }}>
<p class={css({ margin: "0", color: "panel.text" })}>
<code>selected</code> renders a dashed border to mark the active card.
</p>
</Card>
</div>
<div>
<span class={labelClass}>Muted</span>
<Card muted border="default" header={{ heading: "Locked branch" }}>
<p class={css({ margin: "0", color: "panel.text" })}>
<code>muted</code> dims and desaturates a locked or unavailable card.
</p>
</Card>
</div>
</div>In Svelte, React, and Vue the card’s surface follows its props — flip emphasis or feedback in state and it repaints. Astro renders once on the server, so the same card gets a runtime path instead: render it with the reactive prop (which serializes the surface config into the HTML) and call updateCard() from @pindoba/astro-card/runtime:
import { updateCard } from "@pindoba/astro-card/runtime";
updateCard(cardElement, { emphasis: "primary", feedback: "danger" });
The patch re-runs the card’s connect in the browser and swaps the surface classes on the root and on the header/footer parts that inherit from the card — parts with an explicit surface of their own stay put, exactly like a re-render. Only the surface axes (emphasis, feedback, background, translucent, border, shadow, interactive) are patchable; layout never moves. The same functions work from vanilla JavaScript via @pindoba/core-card.
No re-render, no framework runtime
This card was server-rendered by Astro. The buttons above call updateCard() from @pindoba/astro-card/runtime, which re-runs the card's connect in the browser and swaps the surface classes on the root, header, and footer — the same visual change a prop flip produces in the reactive frameworks.
---
import Card from "../Card.astro";
import Button from "@pindoba/astro-button";
import { flex, stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
const emphases = ["secondary", "primary", "tertiary"] as const;
const feedbacks = [
"neutral",
"primary",
"success",
"warning",
"danger",
] as const;
const labelClass = css({
textStyle: "label.sm",
color: "panel.text.muted",
display: "block",
marginBottom: "xs",
});
---
<div class={stack({ gap: "md" })} data-card-reactive-demo>
<div class={stack({ gap: "sm" })}>
<div>
<span class={labelClass}>Emphasis</span>
<div class={flex({ gap: "sm", wrap: "wrap" })}>
{
emphases.map((emphasis) => (
<Button size="sm" emphasis="secondary" data-set-emphasis={emphasis}>
{emphasis}
</Button>
))
}
</div>
</div>
<div>
<span class={labelClass}>Feedback</span>
<div class={flex({ gap: "sm", wrap: "wrap" })}>
{
feedbacks.map((feedback) => (
<Button size="sm" emphasis="secondary" data-set-feedback={feedback}>
{feedback}
</Button>
))
}
</div>
</div>
</div>
<Card
reactive
border="default"
header={{
heading: "Runtime surface",
subheading: "No re-render, no framework runtime",
}}
footer={{ trailing: "updateCard(el, { emphasis })" }}
>
<p class={css({ margin: "0", color: "panel.text" })}>
This card was server-rendered by Astro. The buttons above call{" "}
<code>updateCard()</code> from <code>@pindoba/astro-card/runtime</code>,
which re-runs the card's connect in the browser and swaps the surface
classes on the root, header, and footer — the same visual change a prop
flip produces in the reactive frameworks.
</p>
</Card>
</div>
<script>
import { updateCard } from "@pindoba/astro-card/runtime";
const EMPHASES = ["primary", "secondary", "tertiary"] as const;
const FEEDBACKS = [
"inherit",
"neutral",
"primary",
"success",
"warning",
"danger",
] as const;
function boot() {
const roots = document.querySelectorAll("[data-card-reactive-demo]");
for (const root of roots) {
if (root.hasAttribute("data-card-reactive-demo-initialized")) continue;
root.setAttribute("data-card-reactive-demo-initialized", "");
root.addEventListener("click", (event) => {
const target = event.target instanceof Element ? event.target : null;
const card = root.querySelector("[data-component='card']");
if (!card) return;
const emphasisButton = target?.closest("[data-set-emphasis]");
if (emphasisButton) {
const value = emphasisButton.getAttribute("data-set-emphasis");
const emphasis = EMPHASES.find((candidate) => candidate === value);
if (emphasis) updateCard(card, { emphasis });
return;
}
const feedbackButton = target?.closest("[data-set-feedback]");
if (feedbackButton) {
const value = feedbackButton.getAttribute("data-set-feedback");
const feedback = FEEDBACKS.find((candidate) => candidate === value);
if (feedback) updateCard(card, { feedback });
}
});
}
}
boot();
document.addEventListener("astro:page-load", boot);
</script>The media prop puts an image on the card — full-bleed, radius-aware, and framework-consistent. Give it a src/alt for the built-in <img> (lazy-loaded, object-fit: cover), or provide custom content (a <picture>, video, carousel) via the media slot (Astro/Vue) or media.children (Svelte/React) — the wrapper still handles bleed, clipping, and corners.
placement — top (default) and bottom are full-bleed blocks; start/end pin the media as a full-height column beside the body (set the column with width, default 33%); background covers the whole card behind the content and turns on a darkening scrim by default so text stays legible (opt out with scrim: false, or opt in on any placement).aspect — auto (intrinsic), square, video (16∶9), portrait (3∶4), or wide (21∶9); fixed ratios keep card grids aligned.fit — cover (default) or contain.inset — renders the media inside the card padding with a corner radius concentric to the card’s own instead of bleeding to the edges (top/bottom placements).The wrapper, image, and scrim are all addressable via passThrough.media / passThrough.mediaImg / passThrough.mediaScrim.
Field notes
A walk through the reserve
The media region bleeds to the card edges and inherits the card's top corners.
Placement “background”
Content sits on a darkening scrim so text stays readable over the photo.
Placement “start”
The image spans the card's full height while the body flows beside it — no wrapper markup needed.
Inset media
inset keeps the image inside the card padding with a corner radius concentric to the card's own.
---
import Card from "../Card.astro";
import Badge from "@pindoba/astro-badge";
import Button from "@pindoba/astro-button";
import { css } from "@pindoba/styled-system/css";
import { grid } from "@pindoba/styled-system/patterns";
---
<div
class={grid({
columns: { base: 1, md: 2 },
gap: "md",
width: "100%",
maxWidth: "52rem",
})}
>
{/* Cover image: full-bleed on top, fixed aspect so grids align */}
<Card
border="default"
media={{
src: "https://picsum.photos/seed/pindoba-forest/800/450",
alt: "Sunlight through a forest canopy",
aspect: "video",
}}
header={{
eyebrow: "Field notes",
heading: "Under the canopy",
subheading: "A walk through the reserve",
}}
>
<p class={css({ color: "panel.text", margin: "0" })}>
The media region bleeds to the card edges and inherits the card's top
corners.
</p>
<Button slot="footer-trailing" size="sm" emphasis="secondary">
Read more
</Button>
</Card>
{/* Background cover with the built-in legibility scrim */}
<Card
radius="lg"
media={{
src: "https://picsum.photos/seed/pindoba-dunes/800/500",
alt: "",
placement: "background",
}}
header={{
heading: "Night expedition",
subheading: "Placement “background”",
}}
passThrough={{ root: { style: css.raw({ minHeight: "14rem" }) } }}
>
<p class={css({ color: "panel.text", margin: "0" })}>
Content sits on a darkening scrim so text stays readable over the photo.
</p>
<Button slot="footer-trailing" size="sm" feedback="primary">
Book now
</Button>
</Card>
{
/* Horizontal: media pinned as a start column (full grid width so the
card reads as a row, not a tall sliver) */
}
<Card
border="default"
media={{
src: "https://picsum.photos/seed/pindoba-river/400/600",
alt: "A river bend seen from above",
placement: "start",
width: "14rem",
}}
header={{ heading: "River survey", subheading: "Placement “start”" }}
passThrough={{ root: { style: css.raw({ gridColumn: "1 / -1" }) } }}
>
<p class={css({ color: "panel.text", margin: "0" })}>
The image spans the card's full height while the body flows beside it — no
wrapper markup needed.
</p>
</Card>
{/* Inset: media inside the padding, concentric corners */}
<Card
border="default"
media={{
src: "https://picsum.photos/seed/pindoba-coast/800/340",
alt: "Coastline at dusk",
aspect: "wide",
inset: true,
}}
header={{ heading: "Coastal preview", subheading: "Inset media" }}
passThrough={{ root: { style: css.raw({ gridColumn: "1 / -1" }) } }}
>
<Badge slot="header-trailing" feedback="success">New</Badge>
<p class={css({ color: "panel.text", margin: "0" })}>
<code>inset</code> keeps the image inside the card padding with a corner radius
concentric to the card's own.
</p>
</Card>
</div>The corner slot floats a control in the card’s top-right corner — a dismiss button, an overflow menu, a status pill — and works even on a headerless card, sitting over the content rather than inside a header row. It’s inset by the card’s own padding so it lands in the true padded corner. Pass it as the corner prop (React/Svelte/Vue) or the corner named slot (Astro/Vue).
The corner slot floats a control in the card's top-right corner — here a dismiss button — and works even with no header, sitting over the content instead of inside a header row.
---
import Card from "../Card.astro";
import Button from "@pindoba/astro-button";
import { X } from "@lucide/astro";
import { css } from "@pindoba/styled-system/css";
---
<div class={css({ width: "100%", maxWidth: "32rem" })}>
<Card border="default" radius="lg">
<Button
slot="corner"
emphasis="ghost"
shape="circle"
size="sm"
aria-label="Dismiss"
>
<X size={16} />
</Button>
<p class={css({ margin: "0", color: "panel.text" })}>
The <code>corner</code> slot floats a control in the card's top-right corner
— here a dismiss button — and works even with no header, sitting over the content
instead of inside a header row.
</p>
</Card>
</div>The passThrough prop provides escape hatches for advanced customization: style accepts any Panda CSS SystemStyleObject applied to any slot, and props forwards arbitrary HTML attributes.
Welcome
A few things you can do right away
---
import Card from "../Card.astro";
import Button from "@pindoba/astro-button";
import { ArrowRight, Check } from "@lucide/astro";
import { stack, flex } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
const steps = [
"Invite teammates and assign roles",
"Import or generate your design tokens",
"Browse the component gallery",
];
---
<div class={css({ width: "100%", maxWidth: "38rem" })}>
<Card
feedback="primary"
border="default"
radius="lg"
shadow="md"
header={{
eyebrow: "Welcome",
heading: "Your workspace is ready",
subheading: "A few things you can do right away",
}}
passThrough={{
root: {
style: {
backgroundImage:
"radial-gradient(130% 150% at 100% 0%, token(colors.primary.surface.ground), token(colors.primary.surface.peak))",
borderColor: "primary.border.bold",
},
},
heading: { style: { color: "primary.text.bold" } },
}}
>
<div class={stack({ gap: "sm", direction: "column" })}>
{
steps.map((step) => (
<div class={flex({ gap: "xs", align: "center" })}>
<span
class={flex({
align: "center",
justify: "center",
color: "primary.text.accent",
})}
>
<Check size={18} />
</span>
<span class={css({ color: "panel.text" })}>{step}</span>
</div>
))
}
</div>
<span
slot="footer-leading"
class={css({ color: "primary.text.muted", fontSize: "sm" })}
>
Takes about 5 minutes
</span>
<Button slot="footer-trailing" feedback="primary" size="sm">
Take the tour
<ArrowRight />
</Button>
</Card>
</div>The Card primitive composes with Avatar, Badge, Button, and Input to express a wide range of patterns. The demos below cover the most common compositions; everything else is a recombination of the same slots.
Avatar in header.leading, timestamp as eyebrow, action buttons in header.trailing, emoji reactions and reply control in the footer. layout: { root: { align: "start" }, leading: { align: "center" } } pins the actions to the top while keeping the avatar centered on the text block.
Build #2,847 passed
@helena · 2h ago
Bundle shaved 4.2 KB after migrating the icon set to ESM, all 312 tests green, and Lighthouse held at 98 across the board. Pulling staging tomorrow morning — anyone want to pair on the rollout plan over coffee?
---
import Card from "../Card.astro";
import Avatar from "@pindoba/astro-avatar";
import Stamp from "@pindoba/astro-stamp";
import Button from "@pindoba/astro-button";
import {
Archive,
Coffee,
Flame,
Paperclip,
Reply,
Rocket,
Sparkles,
Star,
} from "@lucide/astro";
import { stack, flex } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
---
<div class={stack({ gap: "lg", direction: "column", width: "100%" })}>
<Card
border="default"
shadow="sm"
header={{
heading: "Helena Rocha",
subheading: "@helena · 2h ago",
eyebrow: "Build #2,847 passed",
layout: {
root: { align: "start" },
leading: { align: "center" },
},
}}
>
<Avatar
slot="header-leading"
name="Helena Rocha"
src="https://i.pravatar.cc/96?img=32"
size="lg"
/>
<div slot="header-trailing" class={flex({ gap: "2xs", align: "center" })}>
<Button emphasis="ghost" shape="circle" size="sm" feedback="primary">
<Reply />
</Button>
<Button emphasis="ghost" shape="circle" size="sm">
<Archive />
</Button>
</div>
<p class={css({ margin: "0", color: "panel.text" })}>
Bundle shaved 4.2 KB after migrating the icon set to ESM, all 312 tests
green, and Lighthouse held at 98 across the board. Pulling staging
tomorrow morning — anyone want to pair on the rollout plan over coffee?
</p>
<div
slot="footer-leading"
class={flex({ gap: "2xs", align: "center", flex: "1" })}
>
<Stamp size="sm" shape="circle" emphasis="secondary" feedback="primary">
<Sparkles />
</Stamp>
<Stamp size="sm" shape="circle" emphasis="secondary" feedback="success">
<Rocket />
</Stamp>
<Stamp size="sm" shape="circle" emphasis="secondary" feedback="warning">
<Coffee />
</Stamp>
<Stamp size="sm" shape="circle" emphasis="secondary" feedback="warning">
<Star />
</Stamp>
<Stamp size="sm" shape="circle" emphasis="secondary" feedback="danger">
<Flame />
</Stamp>
</div>
<Button
slot="footer-trailing"
emphasis="secondary"
shape="square"
radius="inner"
size="sm"
>
<Paperclip />
</Button>
</Card>
</div>Eyebrow + oversized heading via headingTextStyle="numeric.lg", paired with a small profile row in the footer.
Today
Tomorrow
Friday
---
import Card from "../Card.astro";
import Avatar from "@pindoba/astro-avatar";
import { css } from "@pindoba/styled-system/css";
import { flex } from "@pindoba/styled-system/patterns";
const dates = [
{
eyebrow: "Today",
heading: "Aug 14",
person: { name: "Olivia Reis", src: "https://i.pravatar.cc/96?img=23" },
label: "Stand-up at 10am",
},
{
eyebrow: "Tomorrow",
heading: "Aug 15",
person: { name: "Felipe Ramos", src: "https://i.pravatar.cc/96?img=15" },
label: "Design critique",
},
{
eyebrow: "Friday",
heading: "Aug 18",
person: { name: "Marina Cruz", src: "https://i.pravatar.cc/96?img=44" },
label: "Sprint demo",
},
];
---
<div
class={css({
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: "md",
width: "100%",
})}
>
{
dates.map((d) => (
<Card
border="default"
shadow="sm"
header={{
eyebrow: d.eyebrow,
heading: d.heading,
headingTextStyle: "numeric.lg",
}}
>
<div class={flex({ gap: "xs", align: "center", marginTop: "lg" })}>
<Avatar name={d.person.name} src={d.person.src} size="sm" />
<div class={css({ display: "flex", flexDirection: "column" })}>
<span
class={css({
fontSize: "sm",
fontWeight: "semibold",
color: "panel.text.bold",
lineHeight: "tight",
})}
>
{d.label}
</span>
<span
class={css({
fontSize: "xs",
color: "panel.text.muted",
lineHeight: "tight",
})}
>
{d.person.name}
</span>
</div>
</div>
</Card>
))
}
</div>Avatar cluster on top, then a centered <Banner layout={{ content: { align: "center" } }}> inside the content carries the heading and subheading.
8 added this week
Spot which design tokens are pulling weight across the codebase.
2 in beta
Building blocks ready to ship — plus the ones still cooking.
+5 this month
Designers and engineers nudging Pindoba forward, one PR at a time.
---
import Card from "../Card.astro";
import Avatar from "@pindoba/astro-avatar";
import Banner from "@pindoba/astro-banner";
import { grid } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
const groups = [
{
heading: "42 active tokens",
subheading: "8 added this week",
body: "Spot which design tokens are pulling weight across the codebase.",
faces: [
"https://i.pravatar.cc/96?img=30",
"https://i.pravatar.cc/96?img=31",
"https://i.pravatar.cc/96?img=32",
"https://i.pravatar.cc/96?img=33",
],
},
{
heading: "16 components",
subheading: "2 in beta",
body: "Building blocks ready to ship — plus the ones still cooking.",
faces: [
"https://i.pravatar.cc/96?img=40",
"https://i.pravatar.cc/96?img=41",
"https://i.pravatar.cc/96?img=42",
],
},
{
heading: "94 contributors",
subheading: "+5 this month",
body: "Designers and engineers nudging Pindoba forward, one PR at a time.",
faces: [
"https://i.pravatar.cc/96?img=50",
"https://i.pravatar.cc/96?img=51",
"https://i.pravatar.cc/96?img=52",
"https://i.pravatar.cc/96?img=53",
],
},
];
---
<div
class={css({
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: "md",
width: "100%",
})}
>
{
groups.map((g) => (
<Card
border="default"
shadow="sm"
passThrough={{
root: { style: { textAlign: "center" } },
content: {
style: {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "sm",
},
},
}}
>
<div class={grid({ columns: 2, gap: "2xs", width: "fit-content" })}>
{g.faces.slice(0, 4).map((src, i) => (
<Avatar name={`Member ${i + 1}`} src={src} size="sm" />
))}
</div>
<Banner
layout={{ content: { align: "center" } }}
heading={g.heading}
headingTextStyle="heading.xs"
subheading={g.subheading}
/>
<p
class={css({
margin: "0",
fontSize: "sm",
color: "panel.text.muted",
})}
>
{g.body}
</p>
</Card>
))
}
</div>Header with avatar preview and counter, scrollable list in content via maxHeight + overflowY, primary action pinned in footer.
helena@pindoba.dev
felipe@pindoba.dev
marina@pindoba.dev
olivia@pindoba.dev
bruno@pindoba.dev
camila@pindoba.dev
lucas@pindoba.dev
beatriz@pindoba.dev
rafael@pindoba.dev
sofia@pindoba.dev
---
import Card from "../Card.astro";
import Banner from "@pindoba/astro-banner";
import Avatar from "@pindoba/astro-avatar";
import Group from "@pindoba/astro-group";
import Button from "@pindoba/astro-button";
import { Mail, Plus, UserPlus } from "@lucide/astro";
import { stack, flex } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
const members = [
{
name: "Helena Rocha",
email: "helena@pindoba.dev",
src: "https://i.pravatar.cc/96?img=32",
},
{
name: "Felipe Ramos",
email: "felipe@pindoba.dev",
src: "https://i.pravatar.cc/96?img=15",
},
{
name: "Marina Cruz",
email: "marina@pindoba.dev",
src: "https://i.pravatar.cc/96?img=44",
},
{
name: "Olivia Reis",
email: "olivia@pindoba.dev",
src: "https://i.pravatar.cc/96?img=23",
},
{
name: "Bruno Carvalho",
email: "bruno@pindoba.dev",
src: "https://i.pravatar.cc/96?img=12",
},
{
name: "Camila Andrade",
email: "camila@pindoba.dev",
src: "https://i.pravatar.cc/96?img=47",
},
{
name: "Lucas Mendes",
email: "lucas@pindoba.dev",
src: "https://i.pravatar.cc/96?img=8",
},
{
name: "Beatriz Lima",
email: "beatriz@pindoba.dev",
src: "https://i.pravatar.cc/96?img=49",
},
{
name: "Rafael Costa",
email: "rafael@pindoba.dev",
src: "https://i.pravatar.cc/96?img=11",
},
{
name: "Sofia Almeida",
email: "sofia@pindoba.dev",
src: "https://i.pravatar.cc/96?img=21",
},
];
---
<div class={stack({ gap: "lg", direction: "column", width: "100%" })}>
<Card border="default" shadow="sm" header={{ heading: "Invite to project" }}>
<Group slot="header-leading" variant="stack">
<Avatar name="Pinned 1" src="https://i.pravatar.cc/96?img=60" size="sm" />
<Avatar name="Pinned 2" src="https://i.pravatar.cc/96?img=61" size="sm" />
<Avatar name="Pinned 3" src="https://i.pravatar.cc/96?img=62" size="sm" />
<Avatar size="sm" name="+38" />
</Group>
<Button slot="header-trailing" emphasis="secondary" size="sm">
Add member
<UserPlus />
</Button>
<div class={flex({ justify: "space-between", align: "center" })}>
<span
class={css({
fontSize: "sm",
fontWeight: "semibold",
color: "panel.text.bold",
})}>Workspace members</span
>
<span
class={css({
fontSize: "xs",
color: "panel.text.muted",
fontWeight: "medium",
})}>42 active</span
>
</div>
<div
class={css({
display: "flex",
flexDirection: "column",
gap: "sm",
marginTop: "sm",
maxHeight: "320px",
overflowY: "auto",
paddingRight: "2xs",
})}
>
{
members.map((m) => (
<Banner size="sm" heading={m.name} subheading={m.email}>
<Avatar slot="leading" name={m.name} src={m.src} size="md" />
<Button
slot="trailing"
emphasis="secondary"
shape="circle"
size="sm"
>
<Plus />
</Button>
</Banner>
))
}
</div>
<Button
slot="footer-trailing"
feedback="primary"
emphasis="primary"
size="sm"
>
<Mail />
Send invites
</Button>
</Card>
</div>A minimal card with no header or footer — content is centered via passThrough.content.style. interactive enables hover feedback.
---
import Card from "../Card.astro";
import Banner from "@pindoba/astro-banner";
import { Leaf, Sprout, TreePalm } from "@lucide/astro";
import { css } from "@pindoba/styled-system/css";
const brands = [
{ name: "Folha", icon: Leaf },
{ name: "Coqueiro", icon: TreePalm },
{ name: "Raiz", icon: Sprout },
];
---
<div
class={css({
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: "md",
width: "100%",
})}
>
{
brands.map((b) => {
const BrandIcon = b.icon;
return (
<Card
interactive
border="default"
shadow="sm"
passThrough={{
content: {
style: {
minHeight: "240px",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "lg",
},
},
}}
>
<div
class={css({
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "panel.text.bold",
})}
>
<BrandIcon size={48} />
</div>
<Banner
layout={{ content: { align: "center" } }}
heading={b.name}
headingTextStyle="heading.md"
/>
</Card>
);
})
}
</div>Header and footer are both panel-backed, so each can carry its own background, feedback, and emphasis independent of the card body. Use it for stepped meta rows, filled hero strips, footer action zones, or success-tinted confirmation banners.
Pinned this morning
A subtly stepped header sets the meta block apart from the body without shouting — useful for activity feeds and threaded conversations.
Your workspace is set up and ready to roll.
Filled feedback surfaces work as hero strips — the header carries its own palette while the body stays in the card's neutral context.
284 events processed
The footer is panel-backed too — a stepped strip is a quiet way to mark the action zone or summary footer without a hard divider line.
Order #4,128
A feedback-tinted footer doubles as a confirmation strip — success here, but warning and danger work the same way for nudges and alerts.
---
import Card from "../Card.astro";
import Avatar from "@pindoba/astro-avatar";
import Button from "@pindoba/astro-button";
import Stamp from "@pindoba/astro-stamp";
import { ArrowRight, Check, Ellipsis, Rocket } from "@lucide/astro";
import { css } from "@pindoba/styled-system/css";
import { flex } from "@pindoba/styled-system/patterns";
---
<div
class={css({
display: "grid",
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
gap: "md",
width: "100%",
})}
>
<Card
border="default"
shadow="sm"
header={{
heading: "Helena Rocha",
subheading: "Pinned this morning",
background: "surface.base",
}}
>
<Avatar
slot="header-leading"
name="Helena Rocha"
src="https://i.pravatar.cc/96?img=32"
size="md"
/>
<Button slot="header-trailing" emphasis="ghost" shape="circle" size="sm">
<Ellipsis />
</Button>
<p class={css({ margin: "0", color: "panel.text" })}>
A subtly stepped header sets the meta block apart from the body without
shouting — useful for activity feeds and threaded conversations.
</p>
</Card>
<Card
border="default"
shadow="sm"
header={{
heading: "Welcome aboard",
subheading: "Your workspace is set up and ready to roll.",
feedback: "primary",
emphasis: "primary",
background: "surface.peak",
}}
>
<Stamp
slot="header-leading"
size="md"
shape="square"
feedback="primary"
emphasis="primary"
background="surface.ground"
>
<Rocket />
</Stamp>
<p class={css({ margin: "0", color: "panel.text" })}>
Filled feedback surfaces work as hero strips — the header carries its own
palette while the body stays in the card's neutral context.
</p>
<Button
slot="footer-trailing"
feedback="primary"
emphasis="primary"
size="sm"
>
Get started
<ArrowRight />
</Button>
</Card>
<Card
border="default"
shadow="sm"
header={{
heading: "Daily digest",
subheading: "284 events processed",
}}
footer={{
background: "surface.base",
}}
>
<p class={css({ margin: "0", color: "panel.text" })}>
The footer is panel-backed too — a stepped strip is a quiet way to mark
the action zone or summary footer without a hard divider line.
</p>
<div slot="footer-trailing" class={flex({ gap: "xs", align: "center" })}>
<Button emphasis="ghost" size="sm">Skip</Button>
<Button emphasis="primary" feedback="primary" size="sm">
Open report
</Button>
</div>
</Card>
<Card
border="default"
shadow="sm"
header={{
heading: "Payment received",
subheading: "Order #4,128",
}}
footer={{
feedback: "success",
background: "surface.peak",
}}
>
<Stamp slot="header-leading" size="md" shape="circle" feedback="success">
<Check />
</Stamp>
<p class={css({ margin: "0", color: "panel.text" })}>
A feedback-tinted footer doubles as a confirmation strip — success here,
but warning and danger work the same way for nudges and alerts.
</p>
<Button
slot="footer-trailing"
emphasis="ghost"
feedback="success"
size="sm"
>
View receipt
<ArrowRight />
</Button>
</Card>
</div>The card header supports an eyebrow — a small kicker line above the heading. It is delegated to the underlying Banner component, so it inherits panel-aware text color and auto-flips to contrast under emphasis="primary". Pass it as a string or as { content, leading, trailing } to add an icon.
<Card header={{
eyebrow: "Wednesday",
heading: "Dec 14",
}} />falseMark this as the chosen one in a set (a current nav item, a checked option, a selected card). Where `interactive` says it *can* be clicked, `active` says it *is* the current choice. Use `activeEmphasis` to control how loud the state is. Combined with `interactive`, hover and press restart their stepping from the active baseline rather than falling back to the unchosen ramp. The three values differ only in what drives the state: `true` by the prop (via `data-panel-active`), `"checked"` by a wrapped native input (`:has(input:checked)` — zero JS, nothing to re-render), and `"current"` by an existing `aria-current`.
the panel's `emphasis`How loud the `active` state is. Defaults to the panel's own `emphasis`, so the chosen state lands on the same ramp as the resting look — this prop is the override for when the active item should be louder (or quieter) than the panel itself. `"primary"` fills with the feedback accent ramp and flips text to the contrast scale (~150 RGB units of separation — the option that reads at a glance in a dense list). `"secondary"` tints the feedback surface ramp and `"tertiary"` the neutral ramp; both promote text to `bold` and lift the border to the default line, but move the surface only ~7–18 units.
"div"Root element tag. Set `"label"` to make the whole card a control.
Surface background from the Panel scale (`surface.peak` → `surface.ground`, or `transparent`).
Border style: `none`, `default`, `bold`, or `muted`.
No description yet.
Border style applied on hover / focus interaction.
No description yet.
Absolutely-positioned top-right overlay; renders even without a header.
The card's root DOM element — bind with `bind:element`.
Visual emphasis / prominence level.
Semantic color tone: `neutral`, `primary`, `success`, `warning`, `danger`, or `inherit`.
No description yet.
No description yet.
Enable interactive (hover / focus / press) affordances and states.
Media region — an image (or custom media snippet via `children`) placed full-bleed at the top/bottom, as a start/end column, or as a background cover behind the content.
Layout hook for the media region — flips the root layout for `start`/ `end` columns and layers siblings for `background`. Set automatically by the high-level Card from `media.placement`; only set it manually when composing primitives.
"33%"Column width of a `start`/`end` media placement (any CSS length or percentage). Set automatically by the high-level Card from `media.width`.
falseMuted state — dims and desaturates the whole card (locked / inactive / unavailable). Purely visual; pair with `aria-disabled`/`inert` for semantics.
Inner padding from the spacing scale.
Per-slot style and attribute override bag. Each slot accepts `style` (a Panda CSS `SystemStyleObject`) and `props` (extra HTML attributes) merged onto the slot's element.
Corner radius override from the spacing scale (or `full` for fully rounded).
Corner radius for the bottom-left and bottom-right corners.
Corner radius for the top-left and bottom-left corners.
Corner radius for the top-right and bottom-right corners.
Corner radius for the top-left and top-right corners.
falseSerialize the card's surface config into the rendered HTML so `updateCard()` can change `emphasis`/`feedback`/`background` (and the other surface axes) at runtime. Meant for Astro / vanilla JS, where props don't re-render; reactive frameworks don't need it.
Keep (or drop) the always-1px transparent border the panel reserves so a border appearing or changing never shifts layout. Computed automatically — it's reserved when a resting `border` is visible, or when `interactive` / `borderInteract` / `active` can change the border at runtime. Set `false` only on a panel that must not occupy that 1px (e.g. a borderless housing frame that would otherwise add 2px around a set).
falseSelected state — renders a dashed border. Only visible when the card also has a `border` (e.g. `border="default"`); the dashed style rides the existing ring rather than adding one.
Elevation shadow applied to the surface.
"md"Controls the padding of the header, content, and footer sections (and the corner-overlay inset). Scales from `2xs` (densest) through `xs` / `sm` / `md` (default) to `lg` (most generous).
Apply a frosted-glass effect with backdrop blur over a surface background.
Plus all standard <div> HTML
attributes.