Packed.ui
← Components

Card

The warm surface container — Packed's workhorse layout primitive. White surface, soft border, generous radius.

In progress

Lisbon

Portugal

MQ
Mara Quinn
Jun 14 – 2147 n

Boarding TP1234 · Gate B7

Upcoming

Barcelona

Spain

TR
Tom Reyes
Jul 3 – 1027 n

Departs TP8821 · 08:45

8
Active trips
bunx shadcn@latest add https://ui.trypacked.dev/r/card.json

components/ui/card.tsx

import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import type * as React from "react";

import { cn } from "@/registry/lib/utils";

function Card({
  className,
  size = "default",
  padded = true,
  interactive = false,
  render,
  ...props
}: useRender.ComponentProps<"div"> & {
  size?: "default" | "sm";
  padded?: boolean;
  interactive?: boolean;
}) {
  return useRender({
    defaultTagName: "div",
    props: mergeProps<"div">(
      {
        className: cn(
          "group/card flex flex-col overflow-hidden rounded-lg border border-border-subtle bg-card text-card-foreground shadow-card",
          size === "sm"
            ? "[--card-spacing:--spacing(4)]"
            : "[--card-spacing:--spacing(5)]",
          padded && "gap-(--card-spacing) p-(--card-spacing)",
          interactive &&
            "cursor-pointer transition-all duration-[var(--dur-base)] ease-[var(--ease-out)] hover:-translate-y-0.5 hover:shadow-card-hover motion-reduce:transition-none motion-reduce:hover:translate-y-0",
          className,
        ),
      },
      props,
    ),
    render,
    state: {
      slot: "card",
      size,
      padded,
      interactive,
    },
  });
}

function CardMedia({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-media"
      className={cn(
        "relative aspect-[4/3] w-full overflow-hidden bg-surface-sunken",
        className,
      )}
      {...props}
    />
  );
}

function CardMediaOverlay({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-media-overlay"
      className={cn(
        "absolute inset-0 flex flex-col justify-end bg-linear-to-t from-black/55 via-black/15 to-black/5 p-4",
        className,
      )}
      {...props}
    />
  );
}

function CardMediaAction({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-media-action"
      className={cn("absolute top-4 right-4 z-10", className)}
      {...props}
    />
  );
}

function CardIcon({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="card-icon"
      className={cn(
        "flex size-10 shrink-0 items-center justify-center rounded-md bg-brand-subtle text-brand [&_svg]:size-5",
        className,
      )}
      {...props}
    />
  );
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-header"
      className={cn(
        "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-(--card-spacing)",
        "group-data-[padded=false]/card:px-(--card-spacing) group-data-[padded=false]/card:pt-(--card-spacing)",
        className,
      )}
      {...props}
    />
  );
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-title"
      className={cn(
        "font-semibold leading-none text-strong group-data-[size=sm]/card:text-sm",
        className,
      )}
      {...props}
    />
  );
}

function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-description"
      className={cn("text-sm text-muted-text", className)}
      {...props}
    />
  );
}

function CardAction({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-action"
      className={cn(
        "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
        className,
      )}
      {...props}
    />
  );
}

function CardMeta({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-meta"
      className={cn(
        "flex flex-wrap items-center gap-3 text-sm text-muted-text",
        className,
      )}
      {...props}
    />
  );
}

function CardContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-content"
      className={cn(
        "group-data-[padded=false]/card:px-(--card-spacing)",
        className,
      )}
      {...props}
    />
  );
}

function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-footer"
      className={cn(
        "flex items-center gap-2 border-t border-border-subtle",
        "group-data-[padded=true]/card:-mx-(--card-spacing) group-data-[padded=true]/card:px-(--card-spacing) group-data-[padded=true]/card:pt-4",
        "group-data-[padded=false]/card:mx-(--card-spacing) group-data-[padded=false]/card:px-0 group-data-[padded=false]/card:py-4",
        className,
      )}
      {...props}
    />
  );
}

export {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardIcon,
  CardMedia,
  CardMediaAction,
  CardMediaOverlay,
  CardMeta,
  CardTitle,
};

@base-ui/react

  • @packed/utils
  • registry/ui/card.tsx → components/ui/card.tsx