Light

Card

Displays a card with header, content, and footer.

Spec · from metadata

When to use

  • Grouping related content into a visual section
  • Wrapping forms (all forms should live inside a Card)
  • Dashboard widgets or summary panels
  • Content that needs visual separation from the page background

When not to use

  • Simple layout grouping that does not need a border — use a plain div
  • Full-width page sections — Card adds padding and border that may not be appropriate
  • Modal content — use Dialog instead

Anti-patterns

Avoid<Card>
  <h2 className="text-lg font-bold px-6">Title</h2>
  <div className="px-6">Content</div>
</Card>
Prefer<Card>
  <CardHeader>
    <CardTitle>Title</CardTitle>
  </CardHeader>
  <CardContent>Content</CardContent>
</Card>

Use Card sub-components for consistent spacing and data-slot attributes.

Avoid<Card>
  <Card>Nested card</Card>
</Card>
Prefer<Card>Content</Card>

Do not nest Cards. Use visual hierarchy within a single Card instead.

Accessibility

  • Screen readerCard is a plain div — add role='region' and aria-label if it represents a distinct landmark.

Token bindings

TokenCategoryUsage
bg-cardcolorCard background
text-card-foregroundcolorCard text color
rounded-xlradiusCard border radius
shadow-smshadowCard elevation
gap-6 / py-6 / px-6spacingInternal spacing

[!NOTE] When using accent colors (like icons or highlights), they will automatically use bg-primary (Helix navy blue).

Import

import {
  Card,
  CardAction,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
  CardFooter,
} from "@timelycare/helix-ui"

Props

All Card sub-components extend React.ComponentProps<"div"> — they accept any standard div attribute plus className for styling overrides.

// Card — root container
function Card({ className, ...props }: React.ComponentProps<"div">)

// CardHeader — title, description, and optional action
function CardHeader({ className, ...props }: React.ComponentProps<"div">)

// CardTitle — card heading text
function CardTitle({ className, ...props }: React.ComponentProps<"div">)

// CardDescription — helper text under the title
function CardDescription({ className, ...props }: React.ComponentProps<"div">)

// CardAction — top-right header slot for secondary actions
function CardAction({ className, ...props }: React.ComponentProps<"div">)

// CardContent — main card body
function CardContent({ className, ...props }: React.ComponentProps<"div">)

// CardFooter — actions and secondary content at the bottom
function CardFooter({ className, ...props }: React.ComponentProps<"div">)

Variants

VariantUse ForTailwind
defaultAll standard card implementationsbg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm

Choosing a Variant

Cards are container components - visual variations come from content composition, not card variants.


Styling

Card Container

PropertyValueTailwind
LayoutVertical flex columnflex flex-col
Backgroundbg-cardLight: white, Dark: #18181b
Border1px solidborder
Border Radius12pxrounded-xl
ShadowSmall shadowshadow-sm
Section Gap24pxgap-6
Vertical Padding24pxpy-6
Horizontal Padding0pxContent sections handle padding via px-6

Typography

ElementWeightSizeLine HeightColor
Titlefont-semibold (600)Inherited (base)leading-nonetext-card-foreground (inherited)
DescriptionNormal (400)text-sm (14px)Default text-sm line heighttext-muted-foreground

Spacing

PropertyTailwindPixels
Card vertical paddingpy-624px
Section horizontal paddingpx-624px
Gap between sectionsgap-624px
CardHeader internal gap (title to description)gap-28px

Sizes

The Card component does not have a size prop. It uses a single default size. To create denser cards, override spacing via className.

SizeSpacingUse For
defaultStandard padding (py-6, px-6, gap-6)All card uses — forms, content, dashboards

Width

ApproachUse ForNotes
FlexibleAll use casesWidth/height adapt to container and content
Fixed WidthForms, dialogsUse w-[420px] or similar constraints

Header Variants

Text Only (Default)

<CardHeader>
  <CardTitle>Title Text</CardTitle>
  <CardDescription>This is a card description.</CardDescription>
</CardHeader>
  • CardHeader uses CSS Grid layout (grid auto-rows-min grid-rows-[auto_auto])
  • Title/description stack vertically with gap-2 (8px)

With CardAction

CardAction places content (button, badge, link) in the top-right corner of the header:

<CardHeader>
  <CardTitle>Title Text</CardTitle>
  <CardDescription>This is a card description.</CardDescription>
  <CardAction>
    <Button variant="link">Sign Up</Button>
  </CardAction>
</CardHeader>
  • When CardAction is present, CardHeader switches to a two-column grid via has-data-[slot=card-action]:grid-cols-[1fr_auto]
  • CardAction is positioned at the inline-end of the header row (col-start-2 row-span-2 row-start-1)
  • Use for secondary actions: links, badges, icon buttons

With Icon

<CardHeader className="flex-row gap-2 items-center">
  <Icon className="size-12 text-primary" />
  <div className="flex flex-col gap-1">
    <CardTitle>Title Text</CardTitle>
    <CardDescription>This is a card description.</CardDescription>
  </div>
</CardHeader>
  • Icon size: size-12 (48×48px)
  • Icon color: text-primary (Helix navy)
  • Gap between icon and text: gap-2 (8px)

States

StateImplementation
DefaultBase appearance with shadow
Disabledopacity-50 pointer-events-none on Card container

Cards are container components with no interactive states. Child elements (buttons, links) have their own states.

Disabled Card

<Card className="opacity-50 pointer-events-none">
  <CardHeader>
    <CardTitle>Premium Feature</CardTitle>
    <CardDescription>Upgrade to access this feature.</CardDescription>
  </CardHeader>
  <CardContent>
    {/* Content */}
  </CardContent>
</Card>

Common Patterns

Basic Card

<Card>
  <CardHeader>
    <CardTitle>Title Text</CardTitle>
    <CardDescription>This is a card description.</CardDescription>
  </CardHeader>
  <CardContent>
    <p>Your content goes here.</p>
  </CardContent>
  <CardFooter>
    <Button>Action</Button>
  </CardFooter>
</Card>

Card with Icon Header

<Card className="w-[420px]">
  <CardHeader className="flex-row gap-2 items-center">
    <SquareTerminal className="size-12 text-primary" />
    <div className="flex flex-col gap-1">
      <CardTitle>Title Text</CardTitle>
      <CardDescription>This is a card description.</CardDescription>
    </div>
  </CardHeader>
  <CardContent>
    <p>Your content goes here.</p>
  </CardContent>
  <CardFooter>
    <Button>Action</Button>
  </CardFooter>
</Card>

Login Card with CardAction

<Card className="w-[420px]">
  <CardHeader>
    <CardTitle>Login to your account</CardTitle>
    <CardDescription>
      Enter your email below to login to your account
    </CardDescription>
    <CardAction>
      <Button variant="link">Sign Up</Button>
    </CardAction>
  </CardHeader>
  <CardContent className="space-y-6">
    <div className="space-y-2">
      <Label htmlFor="email">Email</Label>
      <Input id="email" placeholder="m@example.com" />
    </div>
    <div className="space-y-2">
      <div className="flex justify-between">
        <Label htmlFor="password">Password</Label>
        <a href="#" className="text-sm text-muted-foreground underline">
          Forgot your password?
        </a>
      </div>
      <Input id="password" type="password" />
    </div>
  </CardContent>
  <CardFooter className="flex-col gap-2">
    <Button className="w-full">Login</Button>
    <Button variant="outline" className="w-full">Login with Google</Button>
    <p className="text-sm text-card-foreground pt-4 text-center">
      Don't have an account? <a href="#" className="underline">Sign up</a>
    </p>
  </CardFooter>
</Card>

Card with Full-Bleed Image

<Card className="w-[420px]">
  <CardHeader className="flex-row gap-2 items-center">
    <SquareTerminal className="size-12 text-primary" />
    <div className="flex flex-col gap-1">
      <CardTitle>Is this an image?</CardTitle>
      <CardDescription>This is a card with an image.</CardDescription>
    </div>
  </CardHeader>
  <CardContent className="px-0">
    <AspectRatio ratio={16/9}>
      <img
        src="/image.jpg"
        alt="Property"
        className="absolute inset-0 size-full object-cover"
      />
    </AspectRatio>
  </CardContent>
  <CardFooter className="flex justify-between">
    <div className="flex gap-2">
      <Badge variant="outline"><Bed className="size-3" /> 4</Badge>
      <Badge variant="outline"><Bath className="size-3" /> 2</Badge>
      <Badge variant="outline"><LandPlot className="size-3" /> 350m²</Badge>
    </div>
    <span className="font-semibold">$135,000</span>
  </CardFooter>
</Card>

Card with Inline Image

<Card className="w-[420px]">
  <CardContent>
    <AspectRatio ratio={2/1} className="rounded-md overflow-clip">
      <img
        src="/image.jpg"
        alt="Description"
        className="absolute inset-0 size-full object-cover"
      />
    </AspectRatio>
  </CardContent>
</Card>
  • Inline images use rounded-md (8px) border radius
  • Full-bleed images remove horizontal padding with px-0 on CardContent

Meeting Notes Card

<Card className="w-[420px]">
  <CardHeader className="flex-row gap-2 items-center">
    <SquareTerminal className="size-12 text-primary" />
    <div className="flex flex-col gap-1">
      <CardTitle>Meeting Notes</CardTitle>
      <CardDescription>Transcript from the meeting with the client.</CardDescription>
    </div>
  </CardHeader>
  <CardContent>
    <div className="text-sm leading-5 text-card-foreground space-y-2">
      <p>Client requested dashboard redesign with focus on mobile responsiveness.</p>
      <ol className="list-decimal list-inside space-y-1">
        <li>New analytics widgets for daily/weekly metrics</li>
        <li>Simplified navigation menu</li>
        <li>Dark mode support</li>
        <li>Timeline: 6 weeks</li>
        <li>Follow-up meeting scheduled for next Tuesday</li>
      </ol>
    </div>
  </CardContent>
  <CardFooter>
    <AvatarGroup />
  </CardFooter>
</Card>

Card with Form

Forms should always be wrapped in a Card for visual grouping.

<Card className="max-w-[374px]">
  <CardHeader>
    <CardTitle>Create Account</CardTitle>
    <CardDescription>Enter your details below.</CardDescription>
  </CardHeader>
  <CardContent className="space-y-4">
    <div className="space-y-2">
      <Label htmlFor="email">Email</Label>
      <Input id="email" placeholder="name@example.com" />
    </div>
    <div className="space-y-2">
      <Label htmlFor="password">Password</Label>
      <Input id="password" type="password" />
    </div>
  </CardContent>
  <CardFooter>
    <Button className="w-full">Create Account</Button>
  </CardFooter>
</Card>

Form Spacing Rules

  • Card padding: py-6 vertical on Card, px-6 horizontal on each section
  • Between fields: space-y-4 (16px)
  • Label to input: space-y-2 (8px)
  • Content to footer: automatic via Card structure

Dashboard Data Card

A compact card for displaying a single key metric with a label, prominent value, and optional trend or badge. Uses existing Card sub-components — no new variant needed.

<Card>
  <CardHeader className="gap-1.5">
    <CardDescription>Total Revenue</CardDescription>
    <CardTitle className="text-3xl font-semibold tracking-tight">
      $45,231.89
    </CardTitle>
  </CardHeader>
  <CardContent>
    <p className="text-xs text-muted-foreground">+20.1% from last month</p>
  </CardContent>
</Card>
  • CardDescription renders before CardTitle so the label sits above the value
  • CardTitle is sized up to text-3xl (30px/36px, Adelle Sans Bold) with tracking-tight
  • CardHeader uses gap-1.5 (6px) for a tighter label-to-value relationship
  • Trend line in CardContent uses text-xs text-muted-foreground (12px, Adelle Sans Regular)

Dashboard Card with Badge

Adds a status or trend badge to the header via CardAction:

<Card>
  <CardHeader className="gap-1.5">
    <CardDescription>Active Users</CardDescription>
    <CardTitle className="text-3xl font-semibold tracking-tight">
      2,350
    </CardTitle>
    <CardAction>
      <Badge variant="secondary">
        <TrendingUp />
        +12.5%
      </Badge>
    </CardAction>
  </CardHeader>
  <CardContent>
    <p className="text-xs text-muted-foreground">+180 since last hour</p>
  </CardContent>
</Card>
  • Badge sits in CardAction (top-right corner of the header grid)
  • Use variant="secondary" for neutral trends, variant="destructive" for negative trends
  • Badge icons (TrendingUp, TrendingDown) auto-size to 12px via [&>svg]:size-3

Dashboard Card with Icon

Places an icon alongside the label for quick visual identification:

<Card>
  <CardHeader className="gap-1.5">
    <div className="flex items-center justify-between">
      <CardDescription>Total Revenue</CardDescription>
      <DollarSign className="size-4 text-muted-foreground" />
    </div>
    <CardTitle className="text-3xl font-semibold tracking-tight">
      $45,231.89
    </CardTitle>
  </CardHeader>
  <CardContent>
    <p className="text-xs text-muted-foreground">+20.1% from last month</p>
  </CardContent>
</Card>
  • Icon is size-4 (16px) in text-muted-foreground — keeps it secondary to the value
  • Placed inline with the label via flex items-center justify-between

Dashboard Card Grid

Dashboard cards are typically arranged in a responsive grid:

<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
  <Card>
    <CardHeader className="gap-1.5">
      <div className="flex items-center justify-between">
        <CardDescription>Total Revenue</CardDescription>
        <DollarSign className="size-4 text-muted-foreground" />
      </div>
      <CardTitle className="text-3xl font-semibold tracking-tight">
        $45,231.89
      </CardTitle>
    </CardHeader>
    <CardContent>
      <p className="text-xs text-muted-foreground">+20.1% from last month</p>
    </CardContent>
  </Card>
  {/* Repeat for each metric */}
</div>
  • Use grid gap-4 sm:grid-cols-2 lg:grid-cols-4 for a responsive 1→2→4 column layout
  • Cards stretch to fill grid cells — no fixed width needed
  • Keep gap-4 (16px) between cards for visual breathing room

Dashboard Card Spacing Rules

PropertyTailwindPixelsNotes
Card vertical paddingpy-624pxDefault; use py-4 for compact
Header internal gapgap-1.56pxTighter than default gap-2
Section horizontal paddingpx-624pxInherited from Card defaults
Grid gapgap-416pxBetween cards in a grid layout

Dashboard Card Typography

ElementTailwindSizeWeightFont
Labeltext-sm text-muted-foreground14px / 20pxNormal (400)Adelle Sans Regular
Valuetext-3xl font-semibold tracking-tight30px / 36pxSemibold (600)Adelle Sans Bold
Trend texttext-xs text-muted-foreground12px / 16pxNormal (400)Adelle Sans Regular
Badge(inherits from Badge)12px / 16pxMedium (500)Adelle Sans Bold

Dark Mode

ElementTokenLightDark
Backgroundbg-cardwhitezinc-900
Borderborder-borderzinc-200white/10
Card Foregroundtext-card-foregroundzinc-950zinc-50
Muted Foregroundtext-muted-foregroundzinc-500zinc-400

Dark mode colors are applied automatically via CSS variables when the dark class is present on a parent element. Always use semantic tokens (bg-card, text-card-foreground) — never hard-code hex values.


Accessibility

  • Styled container — no Radix interactive primitive
  • Use semantic elements: <h3> in CardTitle, <p> in CardDescription
  • If card is interactive, ensure entire card is focusable or use a link wrapper
  • No special ARIA required for static display cards

Gotchas

ProblemSolution
Card too wideSet explicit width via className="w-[420px]" or use grid/flex container
Image has side paddingAdd px-0 to CardContent for full-bleed images
Footer buttons not alignedUse flex justify-between or flex-col gap-2 on CardFooter
Need compact cardOverride spacing via className — e.g. <Card className="py-4 gap-4">
Header icon misalignedAdd flex-row gap-2 items-center to CardHeader
Title/description gap wrongWrap in div with flex flex-col gap-1 when using icon header

See Also

  • Related Components: Dialog (card with modal), Form (card with form)
  • Patterns: Layouts — Card layout patterns
  • Tokens: Colors — Theme-aware color tokens

Last updated: February 24, 2026