PixelCard

Container card with title, optional icon, description, media, ribbon badge, body, and footer — surfaces as <article>, <a href>, or role="button" depending on props.

  • Pixel + linear surfaces with optional tone tint on border and soft background
  • Polymorphic root: renders as <article>, <a href>, or interactive role="button" with Enter/Space activation
  • Media slot, corner ribbon badge, clamped description, padding scale, and composable Header/Body/Footer subcomponents
  • Focus-visible ring + keyboard parity when interactive or anchored
Status
stable
Category
cards
Since
v1.0.0

API

import { PixelCard } from '@pxlkit/ui-kit';

PixelCard

PropTypeDefaultDescription
titlestring—Heading rendered inside the auto-generated header. Omit for a headerless container/well card.
iconReact.ReactNode—Optional leading icon for the auto-generated header.
childrenReact.ReactNode—Card body content.
footerReact.ReactNode—Optional footer slot rendered after the body with a divider.
surface'pixel' | 'linear'—Visual surface override. Inherits from provider when omitted.
tone'neutral' | 'green' | 'cyan' | 'gold' | 'red' | 'purple' | 'pink'—Optional tone tint applied to border + soft background.
interactiveboolean—When true, adds a hover lift + focus ring. If no href is set, an onClick is REQUIRED — the card renders with role="button" + tabIndex={0} + Enter/Space activation for keyboard parity.
mediaReact.ReactNode—Top media slot rendered above the header. Clipped by overflow:hidden.
badge{ label: string; tone?: ToneKey }—Corner ribbon badge — renders PixelRibbon.
descriptionstring—Muted paragraph rendered under the title.
descriptionLines2 | 3 | 4—Apply line-clamp-N + min-h-[N em] to the description.
hrefstring—When provided, the root renders as <a href> instead of <article>. ⚠️ Nesting interactive children (PixelButton, PixelTextLink, etc.) inside footer / media / children is invalid HTML in href mode — screen readers cannot navigate nested interactives inside an anchor. Render those outside the card when you need an actionable area.
target'_self' | '_blank' | '_parent' | '_top' | (string & {})—Anchor target — only meaningful when href is set.
relstring—Anchor rel — only meaningful when href is set.
padding'none' | 'sm' | 'md' | 'lg'—Padding scale; default keeps the legacy p-4 rhythm.
borderedbooleantrueRender with surface-aware border + radius chrome. Defaults to true — a card should look like a card.

Also takes the native attributes and event handlers of its element (HTMLAttributes<HTMLElement>).

ref points to its element (HTMLElement | HTMLAnchorElement).

PixelCard.Header

No props of its own.

Also takes the native attributes and event handlers of its element (HTMLAttributes<HTMLElement>).

PixelCard.Body

No props of its own.

Also takes the native attributes and event handlers of <div> (HTMLAttributes<HTMLDivElement>).

PixelCard.Footer

No props of its own.

Also takes the native attributes and event handlers of its element (HTMLAttributes<HTMLElement>).

Accessibility

WCAG target: 2.1 AA

  • button

When interactive without href, the root renders as <div role="button" tabindex="0"> with Enter/Space activation parity (<article> does not permit role="button"). When href is set, the root renders as a native <a> — nesting interactive children (buttons, links) inside footer or media is invalid in href mode and breaks screen reader navigation. Focus-visible ring is provided automatically in both interactive modes.

Keyboard

KeyActionWhen
EnterActivates the card when interactive (role="button") or follows the link when href is set—
SpaceActivates the card when interactive (role="button")interactive prop is true and href is not set
TabMoves focus to the card when interactive or anchored—

Usage

import { PixelCard } from '@pxlkit/ui-kit';

export function Default() {
  return (
    <PixelCard title="Project Atlas">
      <p>Compact dossier on the Atlas migration. Status nominal.</p>
    </PixelCard>
  );
}

Examples

Default

import { PixelCard } from '@pxlkit/ui-kit';

export function Default() {
  return (
    <PixelCard title="Project Atlas">
      <p>Compact dossier on the Atlas migration. Status nominal.</p>
    </PixelCard>
  );
}

Headerless

import { PixelCard } from '@pxlkit/ui-kit';

export function Headerless() {
  return (
    <PixelCard>
      <p>Omit the title to get a plain well container — no header, no divider.</p>
    </PixelCard>
  );
}

With Icon

import { PixelCard } from '@pxlkit/ui-kit';

export function WithIcon() {
  const Icon = (
    <svg viewBox="0 0 8 8" shapeRendering="crispEdges" fill="currentColor" className="h-3 w-3">
      <rect x="3" y="0" width="2" height="8" />
      <rect x="0" y="3" width="8" height="2" />
    </svg>
  );
  return (
    <PixelCard title="System Health" icon={Icon}>
      <p>All checks green. Last sync 3 minutes ago.</p>
    </PixelCard>
  );
}

With Description

import { PixelCard } from '@pxlkit/ui-kit';

export function WithDescription() {
  return (
    <PixelCard
      title="Release Notes"
      description="A short summary of what changed in this release, useful as a card subtitle."
    >
      <p>Body content sits under the description.</p>
    </PixelCard>
  );
}

Tones

import { PixelCard } from '@pxlkit/ui-kit';

export function Tones() {
  return (
    <div className="grid grid-cols-2 gap-3">
      <PixelCard title="Cyan" tone="cyan">Tinted border + soft background.</PixelCard>
      <PixelCard title="Green" tone="green">Tinted border + soft background.</PixelCard>
      <PixelCard title="Gold" tone="gold">Tinted border + soft background.</PixelCard>
      <PixelCard title="Purple" tone="purple">Tinted border + soft background.</PixelCard>
    </div>
  );
}

Surfaces

import { PixelCard } from '@pxlkit/ui-kit';

export function Surfaces() {
  return (
    <div className="grid grid-cols-2 gap-3">
      <PixelCard title="Pixel" surface="pixel">Thick border + offset shadow.</PixelCard>
      <PixelCard title="Linear" surface="linear">Soft border + smooth radius.</PixelCard>
    </div>
  );
}

Interactive

import { PixelCard } from '@pxlkit/ui-kit';

export function Interactive() {
  return (
    <PixelCard
      title="Click me"
      interactive
      onClick={() => alert('card clicked')}
      description="Press Enter or Space to activate via keyboard."
    >
      <p>Renders as role=button with focus ring.</p>
    </PixelCard>
  );
}

With Media

import { PixelCard } from '@pxlkit/ui-kit';

export function WithMedia() {
  const Media = (
    <div className="h-24 w-full bg-gradient-to-br from-retro-cyan/40 to-retro-purple/40" />
  );
  return (
    <PixelCard title="Cover Story" media={Media} description="Media slot sits above the header.">
      <p>Card body.</p>
    </PixelCard>
  );
}

With Ribbon Badge

import { PixelCard } from '@pxlkit/ui-kit';

export function WithBadge() {
  return (
    <PixelCard
      title="New Feature"
      badge={{ label: 'NEW', tone: 'gold' }}
      description="Ribbon badge anchors in the top-right corner."
    >
      <p>Useful for highlighting fresh content.</p>
    </PixelCard>
  );
}

Clamped Description

import { PixelCard } from '@pxlkit/ui-kit';

export function ClampedDescription() {
  return (
    <PixelCard
      title="Long Description"
      description="This description is intentionally long to demonstrate the line-clamp behavior. It will be truncated to the configured number of lines with an ellipsis, while maintaining a minimum height so cards stay aligned in a grid."
      descriptionLines={2}
    >
      <p>Body still renders below the clamp.</p>
    </PixelCard>
  );
}

Padding Scale

import { PixelCard } from '@pxlkit/ui-kit';

export function PaddingScale() {
  return (
    <div className="grid grid-cols-2 gap-3">
      <PixelCard title="Small" padding="sm">Tight padding.</PixelCard>
      <PixelCard title="Large" padding="lg">Roomy padding.</PixelCard>
    </div>
  );
}

With Subcomponents

import { PixelCard } from '@pxlkit/ui-kit';

export function WithSubcomponents() {
  return (
    <PixelCard title="Composed">
      <PixelCard.Header>
        <span className="text-sm font-semibold">Custom header</span>
      </PixelCard.Header>
      <PixelCard.Body>
        <p>Body slot via subcomponent.</p>
      </PixelCard.Body>
      <PixelCard.Footer>
        <span className="text-xs text-retro-muted">Footer slot</span>
      </PixelCard.Footer>
    </PixelCard>
  );
}

Related