Project Update
Deploy your new project in one click.

Your project is ready for deployment. Review the settings and click deploy to go live.

import { Card, CardBody, CardDescription, CardFooter, CardHeader, CardTitle } from "@registry/components/ui/card";
import { Button } from "@registry/components/ui/button";

export function CardExample() {
  return (
    <Card className="w-full max-w-lg">
      <CardHeader>
        <CardTitle>Project Update</CardTitle>
        <CardDescription>Deploy your new project in one click.</CardDescription>
      </CardHeader>
      <CardBody>
        <p className="text-sm text-gray-600 dark:text-gray-400">
          Your project is ready for deployment. Review the settings and click deploy to go live.
        </p>
      </CardBody>
      <CardFooter>
        <Button variant="secondary">Cancel</Button>
        <Button>Deploy</Button>
      </CardFooter>
    </Card>
  );
}

Examples

With action

Use CardAction to place an action button in the header.

Notifications
Manage your notification preferences.

You have 3 unread notifications. Review them to stay up to date.

import { Card, CardAction, CardBody, CardDescription, CardHeader, CardTitle } from "@registry/components/ui/card";
import { Button } from "@registry/components/ui/button";

export function CardWithActionExample() {
  return (
    <Card className="w-full max-w-lg">
      <CardHeader>
        <CardTitle>Notifications</CardTitle>
        <CardDescription>Manage your notification preferences.</CardDescription>
        <CardAction>
          <Button variant="ghost" size="sm">
            Mark all read
          </Button>
        </CardAction>
      </CardHeader>
      <CardBody>
        <p className="text-sm text-gray-600 dark:text-gray-400">
          You have 3 unread notifications. Review them to stay up to date.
        </p>
      </CardBody>
    </Card>
  );
}

Use direction="column" on CardFooter to stack buttons vertically.

Subscribe
Get the latest updates delivered to your inbox.

Join our newsletter for weekly insights and product updates.

import { Card, CardBody, CardDescription, CardFooter, CardHeader, CardTitle } from "@registry/components/ui/card";
import { Button } from "@registry/components/ui/button";

export function CardColumnFooterExample() {
  return (
    <Card className="w-full max-w-lg">
      <CardHeader>
        <CardTitle>Subscribe</CardTitle>
        <CardDescription>Get the latest updates delivered to your inbox.</CardDescription>
      </CardHeader>
      <CardBody>
        <p className="text-sm text-gray-600 dark:text-gray-400">
          Join our newsletter for weekly insights and product updates.
        </p>
      </CardBody>
      <CardFooter direction="column">
        <Button className="w-full">Subscribe Now</Button>
        <Button variant="secondary" className="w-full">
          Learn More
        </Button>
      </CardFooter>
    </Card>
  );
}

You can wrap the footer in motion.div with AnimatePresence to animate it in and out when conditions change.

Profile Settings
Update your profile information.
import { Card, CardBody, CardDescription, CardFooter, CardHeader, CardTitle } from "@registry/components/ui/card";
import { Button } from "@registry/components/ui/button";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { Input } from "@registry/components/ui/input";

export function CardMotionFooterExample() {
  const [name, setName] = useState("Alex Doe");
  const [originalName] = useState("Alex Doe");
  const hasChanges = name !== originalName;

  return (
    <Card className="w-full max-w-lg">
      <CardHeader>
        <CardTitle>Profile Settings</CardTitle>
        <CardDescription>Update your profile information.</CardDescription>
      </CardHeader>
      <CardBody className="grid gap-4">
        <div className="grid gap-2">
          <label className="text-sm font-medium" htmlFor="name">
            Name
          </label>
          <Input id="name" onChange={(e) => setName(e.target.value)} value={name} />
        </div>
      </CardBody>
      <AnimatePresence>
        {hasChanges && (
          <motion.div
            animate={{ height: "auto", opacity: 1 }}
            className="grid min-h-0 overflow-hidden"
            exit={{ height: "0px", opacity: 0 }}
            initial={{ height: "0px", opacity: 0 }}
            key="save-button"
            transition={{
              duration: 0.3,
              ease: [0.79, 0.14, 0.15, 0.86],
            }}
          >
            <CardFooter>
              <AnimatePresence mode="wait">
                <motion.div
                  animate={{ filter: "blur(0px)", opacity: 1, scale: 1 }}
                  exit={{ filter: "blur(6px)", opacity: 0, scale: 0.95 }}
                  initial={{ filter: "blur(6px)", opacity: 0, scale: 0.95 }}
                  key="reset-button"
                  transition={{
                    duration: 0.3,
                    ease: [0.79, 0.14, 0.15, 0.86],
                  }}
                >
                  <Button onClick={() => setName(originalName)} type="button" variant="ghost">
                    Reset
                  </Button>
                </motion.div>
              </AnimatePresence>
              <AnimatePresence mode="wait">
                <motion.div
                  animate={{ filter: "blur(0px)", opacity: 1, scale: 1 }}
                  exit={{ filter: "blur(6px)", opacity: 0, scale: 0.95 }}
                  initial={{ filter: "blur(6px)", opacity: 0, scale: 0.95 }}
                  key="save-button"
                  transition={{
                    duration: 0.3,
                    ease: [0.79, 0.14, 0.15, 0.86],
                  }}
                >
                  <Button type="submit">Save Changes</Button>
                </motion.div>
              </AnimatePresence>
            </CardFooter>
          </motion.div>
        )}
      </AnimatePresence>
    </Card>
  );
}

Installation

Copy the source code below into your project:

import { mergeProps, useRender } from "@base-ui/react";
import { cn } from "@registry/lib/utils";

function Card({ className, ...props }: useRender.ComponentProps<"div">) {
  const cardElement = useRender({
    defaultTagName: "div",
    props: {
      ...mergeProps<"div">(props, {
        className: cn(
          "relative flex flex-col rounded-3xl border border-transparent bg-card p-1 dark:border-border",
          className,
        ),
      }),
      "data-slot": "card",
    },
  });
  return cardElement;
}

function CardHeader({ className, ...props }: useRender.ComponentProps<"div">) {
  const cardHeaderElement = useRender({
    defaultTagName: "div",
    props: {
      ...mergeProps<"div">(props, {
        className: cn(
          "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-5 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
          className,
        ),
      }),
      "data-slot": "card-header",
    },
  });
  return cardHeaderElement;
}

function CardTitle({ className, ...props }: useRender.ComponentProps<"div">) {
  const cardTitleElement = useRender({
    defaultTagName: "div",
    props: {
      ...mergeProps<"div">(props, {
        className: cn("text-lg leading-none font-semibold", className),
      }),
      "data-slot": "card-title",
    },
  });
  return cardTitleElement;
}

function CardDescription({ className, ...props }: useRender.ComponentProps<"div">) {
  const cardDescriptionElement = useRender({
    defaultTagName: "div",
    props: {
      ...mergeProps<"div">(props, {
        className: cn("text-sm text-muted-foreground", className),
      }),
      "data-slot": "card-description",
    },
  });
  return cardDescriptionElement;
}

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

function CardPanel({ className, children, ...props }: useRender.ComponentProps<"div">) {
  return (
    <div
      className={cn(
        "w-full rounded-[calc(var(--radius-3xl)-var(--spacing))] border border-border bg-popover p-5",
        className,
      )}
      data-slot="card-panel"
      {...props}
    >
      {children}
    </div>
  );
}

function CardFooter({
  className,
  direction = "row",
  ...props
}: useRender.ComponentProps<"div"> & { direction?: "row" | "column" }) {
  const cardFooterElement = useRender({
    defaultTagName: "div",
    props: {
      ...mergeProps<"div">(props, {
        className: cn(
          "flex items-center gap-2 p-5 [.border-t]:pt-5",
          direction === "row" && "flex-row justify-end",
          direction === "column" && "flex-col",
          className,
        ),
      }),
      "data-slot": "card-footer",
    },
  });
  return cardFooterElement;
}

export {
  Card,
  CardHeader,
  CardFooter,
  CardTitle,
  CardAction,
  CardDescription,
  CardPanel,
  CardPanel as CardContent,
  CardPanel as CardBody,
};

API Reference

Card

PropTypeDefaultDescription
classNamestringAdditional CSS classes to apply to the card container.

CardHeader

PropTypeDefaultDescription
classNamestringAdditional CSS classes to apply to the header container.

CardTitle

PropTypeDefaultDescription
classNamestringAdditional CSS classes to apply to the title element.

CardDescription

PropTypeDefaultDescription
classNamestringAdditional CSS classes to apply to the description element.

CardAction

PropTypeDefaultDescription
classNamestringAdditional CSS classes to apply to the action container.

CardBody

CardBody is an alias for CardPanel.

PropTypeDefaultDescription
classNamestringAdditional CSS classes to apply to the panel container.

CardFooter

PropTypeDefaultDescription
direction"row" | "column""row"Layout direction for the footer content.
classNamestringAdditional CSS classes to apply to the footer container.