Card

A composable surface for grouping related content — pricing tiers, profile blocks, stats, callouts, project tiles. The header / body / footer slots compose freely, and the footer anchors to the bottom whenever the card has vertical slack (which makes pricing rows and stat grids line up without any extra wrapper).

Free

Get started in a minute.

1 site, watermarked, hosted on boogie.ro.

Boogie Pro New

For teams who ship motion-rich sites.

Unlimited sites, custom domain, AI rewrites, premium stock library, automatic accessibility checks, and round-the-clock motion support — no watermark, ever.

Anatomy

A Card is just a flex column. The named subcomponents — CardHeader, CardTitle, CardDescription, CardFooter — give you typography and spacing defaults that match the rest of the kit, but everything is optional. Drop in whatever children you want; the only structural rule is that CardFooter auto-pins to the bottom of the card.

Card title

Card description goes here.

Body content sits between the header and the footer.

import {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardFooter,
  Button,
} from '@bwo-ui/react';

<Card>
  <CardHeader>
    <CardTitle>Card title</CardTitle>
    <CardDescription>Card description goes here.</CardDescription>
  </CardHeader>
  <p>Body content sits between the header and the footer.</p>
  <CardFooter>
    <Button size="sm" variant="ghost">Action</Button>
    <Button size="sm" variant="primary">Primary</Button>
  </CardFooter>
</Card>

Interactive

Add the interactive prop to opt into the hover lift treatment — pointer cursor, a deeper shadow, and a 2 px translateY on hover. Use it whenever the entire card is clickable. For accessibility, wire up the click target yourself (typically by wrapping the card in a Link or attaching onClick + role="button" + tabIndex; see the Accessibility section below).

Static

No hover effect — display only.

Renders as a passive surface.

Interactive

Hover for the lift effect.

Cursor turns into a pointer, shadow deepens, and the card lifts 2 px.

<Card>{/* static */}</Card>
<Card interactive>{/* hover-lifted */}</Card>

Padding modes

pad controls the inner padding. The default (24 px) is sized for prose-heavy layouts; compact (12 px) packs cards into dense grids; none lets edge-to-edge content like CardMedia bleed to the card's rounded corners.

Default

24 px inner padding.

Compact

12 px inner padding. Use in dense grids.

None

Edge-to-edge media.

<Card>{/* default — 24 px */}</Card>
<Card pad="compact">{/* 12 px */}</Card>
<Card pad="none">
  <CardMedia aspect="4 / 3">…</CardMedia>
  <div style={{ padding: 14 }}>edge-to-edge media + manually padded text</div>
</Card>

Corner radius

The radius prop accepts the shared radius scale (none / sm / md / lg). Omit it to inherit --bwo-radius-current (defaults to 6 px). Picking a sharper radius is a common cue for editorial / project tiles; rounder cards feel softer and more consumer-app.

radius="none"

Tunes the corner curve.

radius="sm"

Tunes the corner curve.

radius="md"

Tunes the corner curve.

radius="lg"

Tunes the corner curve.

<Card radius="none">…</Card>
<Card radius="sm">…</Card>
<Card radius="md">…</Card>
<Card radius="lg">…</Card>

Footer alignment

CardFooter uses margin-top: auto inside the card's flex column. The practical effect: whenever the card has any vertical slack (typically because its parent stretches sibling cards to equal height — the default for CSS grids), the footer hugs the bottom. The cards below all have different body lengths, but their footers line up.

Starter

One line.

Growth

A few lines of supporting copy that takes a bit more vertical space than the first card.

Scale

A noticeably longer body section that pushes the natural flow further down, with enough copy to wrap across multiple lines and demonstrate that the footer still anchors to the bottom regardless of how tall the card has to grow.

<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
  <Card>
    <CardHeader><CardTitle>Starter</CardTitle></CardHeader>
    <p>Short body.</p>
    <CardFooter><Button size="sm">Pick</Button></CardFooter>
  </Card>
  <Card>
    <CardHeader><CardTitle>Growth</CardTitle></CardHeader>
    <p>Medium body that wraps over two or three lines.</p>
    <CardFooter><Button size="sm">Pick</Button></CardFooter>
  </Card>
  <Card>
    <CardHeader><CardTitle>Scale</CardTitle></CardHeader>
    <p>A noticeably longer paragraph of body copy that wraps across many lines…</p>
    <CardFooter><Button size="sm">Pick</Button></CardFooter>
  </Card>
</div>

The behaviour kicks in automatically when the card has slack. In a standalone card sized purely by content, the footer simply sits beneath the body — same as before. No prop required.

Tile layout

For project / template tiles where the media bleeds to the card edges, switch to pad="none" and use the dedicated tile subcomponents: CardMedia, CardTab, CardEyebrow, CardName, CardCaption.

BOOGIE 0001

Agency

Studio · Light

BOOGIE 0002

Basalt

Construction

BOOGIE 0003

Silica

Tech · Light

import {
  Card,
  CardMedia,
  CardTab,
  CardEyebrow,
  CardName,
  CardCaption,
  MediaZoom,
} from '@bwo-ui/react';

<Card pad="none" radius="sm" interactive>
  <CardMedia aspect="3 / 4">
    <MediaZoom>
      <img src="/tile.jpg" alt="" />
    </MediaZoom>
  </CardMedia>
  <CardTab>
    <CardEyebrow>BOOGIE 0001</CardEyebrow>
    <CardName>Agency</CardName>
    <CardCaption>Studio · Light</CardCaption>
  </CardTab>
</Card>

Recipes

Stat card

A common analytics pattern: uppercase eyebrow + display-font value + small delta line. Compose it directly from a Card — no special prop required.

Monthly visitors

128.4k

+12.3% vs last month

Active sites

4,910

+218 vs last month

Bounce rate

38%

−2.1% vs last month

<Card pad="compact">
  <p style={{ fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--bwo-text-body)' }}>
    Monthly visitors
  </p>
  <p style={{ fontSize: 28, fontWeight: 700, letterSpacing: '-0.02em', margin: '6px 0 4px' }}>
    128.4k
  </p>
  <p style={{ fontSize: 12, fontWeight: 600, color: 'var(--bwo-green)' }}>
    +12.3% vs last month
  </p>
</Card>

Profile card

Avatar + name + role on top, action buttons in the footer. The footer's automatic bottom-alignment is what makes these cards line up cleanly in a directory grid.

Ana Radu

Lead motion designer · Boogie

<Card>
  <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
    <Avatar />
    <div>
      <p style={{ fontSize: 15, fontWeight: 600 }}>Ana Radu</p>
      <p style={{ fontSize: 13, color: 'var(--bwo-text-body)' }}>Lead motion designer</p>
    </div>
  </div>
  <CardFooter>
    <Button size="sm" variant="outline">Message</Button>
    <Button size="sm" variant="primary">Follow</Button>
  </CardFooter>
</Card>

Quote / callout

For testimonial blocks or pull quotes, drop the structured subcomponents and just write prose. The Card's padding and shadow do the visual work.

“bwo-ui shipped 60 components in a month. Every primitive is ours — no Radix, no cmdk, no day-picker. It just feels like one library.”

Mihai Stoica

Frontend lead · boogie.ro

<Card radius="md">
  <p style={{ fontSize: 18, lineHeight: 1.45, fontWeight: 500 }}>
    “bwo-ui shipped 60 components in a month. Every primitive is ours…”
  </p>
  <div style={{ display: 'flex', gap: 10, marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--bwo-border)' }}>
    <Avatar />
    <div>
      <p style={{ fontSize: 13, fontWeight: 600 }}>Mihai Stoica</p>
      <p style={{ fontSize: 12, color: 'var(--bwo-text-body)' }}>Frontend lead · boogie.ro</p>
    </div>
  </div>
</Card>

Aligning footer actions

CardFooter is a flexbox row. It defaults to left-aligned content — override with justifyContent when you want actions on the right or split across both sides.

{/* Right-aligned actions */}
<CardFooter style={{ justifyContent: 'flex-end' }}>
  <Button size="sm" variant="ghost">Cancel</Button>
  <Button size="sm" variant="primary">Save</Button>
</CardFooter>

{/* Split — meta info on the left, primary action on the right */}
<CardFooter style={{ justifyContent: 'space-between' }}>
  <span style={{ fontSize: 12, color: 'var(--bwo-text-body)' }}>Updated 2 h ago</span>
  <Button size="sm" variant="primary">Open</Button>
</CardFooter>

Accessibility

Card props

PropTypeDefaultDescription
interactivebooleanfalseAdds the hover lift effect — pointer cursor, deeper shadow, 2 px translateY. Does not add a11y wiring; see the Accessibility section.
radius'none' | 'sm' | 'md' | 'lg' | 'pill'Corner radius preset. Omit to inherit `--bwo-radius-current` (defaults to 6 px).
pad'default' | 'compact' | 'none''default''default' = 24 px, 'compact' = 12 px, 'none' = no padding (use with `CardMedia` for edge-to-edge media).
…restHTMLAttributes<HTMLDivElement>Every native div attribute is forwarded — `onClick`, `style`, etc.

Subcomponents

PropTypeDefaultDescription
CardHeader<div>Top section above the body. 12 px margin-bottom.
CardTitle<h3>18 px / 600 weight heading.
CardDescription<p>14 px muted line under the title.
CardFooter<div>Bottom section. Auto-pins to the bottom of the card (margin-top: auto) and has a 1 px top divider. Flex row with 8 px gap; align actions via `justifyContent` in inline style.
CardMedia<div aspect>Top media slot. Wraps an inner `<img>` / `<video>` with object-fit: cover. Aspect defaults to 3/4. Pairs with `pad="none"` for edge-to-edge media.
CardTab<div>Strip beneath `CardMedia` — typically eyebrow + name + caption.
CardEyebrow<p>Small uppercase code or label above the name.
CardName<p>Display-font name line.
CardCaption<p>Tiny uppercase category / status tag.