Skip to content
Seamless UI

Card

Save

A hairline surface for grouped content, with header, content, and footer parts and an optional interactive state.

Category
Core
Version
0.2.0
Frameworks
Next.js, Vite + React
Styling
Tailwind CSS v4

Preview

Card preview

Research agent

Answers questions from your docs and cites its sources.

Runs
1,284
Success
97.2%
Median
4.1 s
Updated 2 min agoView runs

Installation

npx @seamlessui/cli add card

Pre-release@seamlessui/cli is not on npm yet. Install from a local build.

Review the plan first. A dry run lists every file and package without changing anything:

npx @seamlessui/cli add card --dry-run

Usage

card-example.tsx
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/seamless/ui/card";

const stats = [
  { label: "Runs", value: "1,284" },
  { label: "Success", value: "97.2%" },
  { label: "Median", value: "4.1 s" },
];

export default function CardExample() {
  return (
    <Card interactive className="w-full max-w-sm">
      <CardHeader>
        <CardTitle>Research agent</CardTitle>
        <CardDescription>Answers questions from your docs and cites its sources.</CardDescription>
      </CardHeader>
      <CardContent>
        <dl className="grid grid-cols-3 divide-x divide-zinc-950/[0.07] rounded-lg border border-zinc-950/[0.07] dark:divide-white/[0.08] dark:border-white/[0.08]">
          {stats.map((stat) => (
            <div key={stat.label} className="grid gap-1 px-3 py-2.5">
              <dt className="text-xs font-medium text-zinc-500">{stat.label}</dt>
              <dd className="font-mono text-sm text-zinc-950 tabular-nums dark:text-white">{stat.value}</dd>
            </div>
          ))}
        </dl>
      </CardContent>
      <CardFooter className="justify-between">
        <span className="text-xs text-zinc-500">Updated 2 min ago</span>
        <a
          href="#runs"
          className="group inline-flex items-center gap-1 rounded-md text-sm font-medium text-zinc-950 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500 dark:text-white"
        >
          View runs
          <svg
            aria-hidden="true"
            viewBox="0 0 16 16"
            fill="none"
            className="size-4 text-zinc-500 transition-[translate,color] duration-150 ease-out group-hover:text-current motion-safe:group-hover:translate-x-0.5 motion-reduce:transition-none"
          >
            <path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </a>
      </CardFooter>
    </Card>
  );
}

API

PropTypeDefaultDescription
interactivebooleanfalseStrengthens the border and shadow on hover and when something inside has keyboard focus. Use when the card contains the primary link or action.
CardHeader, CardTitle, CardDescription, CardContent, CardFooterComponentProps<…>—Layout parts. Each accepts the props of its element (div, h3, or p).

Accessibility

  • CardTitle renders an <h3>; pass a different element with your own heading if the page outline needs it.
  • The interactive state also shows on keyboard focus inside the card (has-focus-visible). It only changes border and shadow color, so nothing moves; the transition is removed with reduced motion.

Files

Installed into src/components/seamless, or components/seamless when the project has no src directory.

  • src/components/seamless/ui/card.tsxcard
  • npm packages: noneReact and Tailwind CSS v4 required

Source

The exact source the CLI installs, from /r/items/react/card/0.2.0.json.

ui/card.tsxShow
ui/card.tsx
import type { ComponentProps } from "react";

function cx(...classes: (string | false | null | undefined)[]) {
  return classes.filter(Boolean).join(" ");
}

export interface CardProps extends ComponentProps<"div"> {
  /**
   * For cards that act as links or buttons: the border and shadow strengthen on hover and
   * when something inside has keyboard focus.
   */
  interactive?: boolean;
}

export function Card({ interactive = false, className, ...props }: CardProps) {
  return (
    <div
      data-interactive={interactive || undefined}
      className={cx(
        "rounded-xl border border-zinc-950/10 bg-white text-zinc-950 shadow-[0_1px_2px_rgb(0_0_0/0.04)]",
        "dark:border-white/10 dark:bg-black dark:text-white dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.06)]",
        interactive &&
          cx(
            "transition-[border-color,box-shadow] duration-150 ease-out motion-reduce:transition-none",
            "hover:border-zinc-950/15 hover:shadow-[0_1px_2px_rgb(0_0_0/0.04),0_6px_16px_-8px_rgb(0_0_0/0.12)]",
            "has-focus-visible:border-zinc-950/15",
            "dark:hover:border-white/20 dark:hover:shadow-[inset_0_1px_0_rgb(255_255_255/0.08)] dark:has-focus-visible:border-white/20",
          ),
        className,
      )}
      {...props}
    />
  );
}

/** Title block. Holds a CardTitle and an optional CardDescription. */
export function CardHeader({ className, ...props }: ComponentProps<"div">) {
  return <div className={cx("flex flex-col gap-1.5 px-5 pt-5", className)} {...props} />;
}

export function CardTitle({ className, ...props }: ComponentProps<"h3">) {
  return <h3 className={cx("text-base leading-6 font-semibold tracking-[-0.01em]", className)} {...props} />;
}

export function CardDescription({ className, ...props }: ComponentProps<"p">) {
  return <p className={cx("text-sm leading-relaxed text-zinc-600 dark:text-zinc-400", className)} {...props} />;
}

export function CardContent({ className, ...props }: ComponentProps<"div">) {
  return <div className={cx("p-5", className)} {...props} />;
}

/** Action row, separated from the content by a divider. */
export function CardFooter({ className, ...props }: ComponentProps<"div">) {
  return (
    <div
      className={cx(
        "flex items-center gap-2 border-t border-zinc-950/[0.07] px-5 py-3 dark:border-white/[0.08]",
        className,
      )}
      {...props}
    />
  );
}