# Card

> Surface with header, content and footer slots and a lit edge.

Category: Data Display. Docs: https://get-neonkit.vercel.app/components/card

## Installation

```bash
npx shadcn@latest add https://get-neonkit.vercel.app/r/card.json
```

With the `@neonkit` namespace configured: `npx shadcn@latest add @neonkit/card`.

npm dependencies: none. Also needs `lib/utils.ts` (`cn`) and `lib/neon.ts` (`neonVars`), which the CLI adds.

## Usage

```tsx
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"

<Card>
  <CardHeader>
    <CardTitle>Usage this month</CardTitle>
    <CardDescription>Pro plan · resets in 20 days</CardDescription>
  </CardHeader>
  <CardContent>…</CardContent>
</Card>
```

## Examples

### Usage

A 1px lit edge runs along the top, brightest in the middle.

```tsx
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";

const metrics = [
  { label: "Bandwidth", used: "182.4 GB", limit: "1 TB", percent: 18 },
  { label: "Edge requests", used: "1.24M", limit: "10M", percent: 12 },
  { label: "Build minutes", used: "4,410", limit: "6,000", percent: 74 },
  { label: "Image optimizations", used: "4,612", limit: "5,000", percent: 92, warn: true },
];

export default function CardDemo() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <div className="flex items-center justify-between gap-4">
          <CardTitle>Usage this month</CardTitle>
          <span className="text-xs text-white/40">Oct 1 – 31</span>
        </div>
        <CardDescription>Pro plan · resets in 20 days</CardDescription>
      </CardHeader>
      <CardContent className="grid gap-5">
        <div>
          <p className="text-3xl font-semibold tabular-nums tracking-tight text-white">$38.20</p>
          <p className="mt-1 text-xs text-white/45">Estimated bill, including $18.20 in overages</p>
        </div>
        <div className="grid gap-4">
          {metrics.map((metric) => (
            <div key={metric.label} className="grid gap-2">
              <div className="flex items-baseline justify-between gap-4 text-sm">
                <span className="text-white/55">{metric.label}</span>
                <span className="tabular-nums text-white/90">
                  {metric.used} <span className="text-white/35">/ {metric.limit}</span>
                </span>
              </div>
              <div className="h-1 overflow-hidden rounded-full bg-white/[0.06]">
                <div
                  className={
                    metric.warn
                      ? "h-full rounded-full bg-[rgb(251_191_36)] shadow-[0_0_8px_rgb(251_191_36/0.6)]"
                      : "h-full rounded-full bg-[rgb(var(--neon))] shadow-[0_0_8px_rgb(var(--neon)/0.6)]"
                  }
                  style={{ width: `${metric.percent}%` }}
                />
              </div>
            </div>
          ))}
        </div>
      </CardContent>
      <CardFooter className="justify-between border-t border-white/[0.06] pt-4">
        <span className="text-xs text-white/40">Updated 4 minutes ago</span>
        <Button variant="ghost" size="sm" className="-mr-3">
          View breakdown
        </Button>
      </CardFooter>
    </Card>
  );
}
```

### Pricing

glow adds a neon border and ambient light. Use it on one card per view.

```tsx
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";

const plans = [
  {
    name: "Hobby",
    price: "$0",
    period: "forever",
    description: "For side projects and trying things out.",
    features: ["3 projects", "Preview deployments", "Community support"],
    cta: "Start for free",
    featured: false,
  },
  {
    name: "Pro",
    price: "$20",
    period: "per seat / month",
    description: "For teams shipping to production every day.",
    features: ["Unlimited projects", "Preview deployments", "Password-protected previews", "Priority support"],
    cta: "Upgrade to Pro",
    featured: true,
  },
];

export default function CardPricing() {
  return (
    <div className="grid w-full max-w-2xl gap-4 sm:grid-cols-2">
      {plans.map((plan) => (
        <Card key={plan.name} variant={plan.featured ? "glow" : "default"}>
          <CardHeader>
            <div className="flex items-center justify-between gap-3">
              <CardTitle>{plan.name}</CardTitle>
              {plan.featured && (
                <span className="rounded-full border border-[rgb(var(--neon)/0.3)] bg-[rgb(var(--neon)/0.1)] px-2 py-0.5 text-[11px] font-medium text-[rgb(var(--neon))]">
                  Most popular
                </span>
              )}
            </div>
            <CardDescription>{plan.description}</CardDescription>
          </CardHeader>
          <CardContent>
            <p className="flex items-baseline gap-1.5">
              <span className="text-4xl font-semibold tabular-nums tracking-tight text-white">{plan.price}</span>
              <span className="text-sm text-white/45">{plan.period}</span>
            </p>
            <ul className="mt-6 grid gap-2.5 text-sm text-white/70">
              {plan.features.map((feature) => (
                <li key={feature} className="flex items-center gap-2.5">
                  <Check
                    aria-hidden
                    className={plan.featured ? "size-4 text-[rgb(var(--neon))]" : "size-4 text-white/35"}
                  />
                  {feature}
                </li>
              ))}
            </ul>
          </CardContent>
          <CardFooter>
            <Button variant={plan.featured ? "solid" : "outline"} className="w-full">
              {plan.cta}
            </Button>
          </CardFooter>
        </Card>
      ))}
    </div>
  );
}
```

### Link cards

interactive lifts the card on hover. A stretched link makes the whole surface clickable.

```tsx
import { ArrowUpRight, Boxes, Package, Palette, Terminal } from "lucide-react";
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

const links = [
  {
    title: "Installation",
    description: "Point the shadcn CLI at the registry and add your first component.",
    href: "#installation",
    icon: Terminal,
  },
  {
    title: "Theming",
    description: "Set --neon-accent once and every component follows it.",
    href: "#theming",
    icon: Palette,
  },
  {
    title: "Components",
    description: "Each one is a single file you own, edit and ship.",
    href: "#components",
    icon: Boxes,
  },
  {
    title: "Registry",
    description: "Serve your own variants as shadcn-compatible JSON.",
    href: "#registry",
    icon: Package,
  },
];

export default function CardInteractive() {
  return (
    <div className="grid w-full max-w-2xl gap-4 sm:grid-cols-2">
      {links.map(({ title, description, href, icon: Icon }) => (
        <Card key={title} interactive className="group">
          <CardHeader className="gap-2">
            <div className="mb-2 flex size-9 items-center justify-center rounded-lg border border-white/10 bg-white/[0.04] text-[rgb(var(--neon))]">
              <Icon aria-hidden className="size-4" />
            </div>
            <CardTitle className="flex items-center justify-between gap-3">
              {/* The link's ::after covers the card, so the whole surface is clickable. */}
              <a href={href} className="outline-none after:absolute after:inset-0 after:rounded-2xl">
                {title}
              </a>
              <ArrowUpRight
                aria-hidden
                className="size-4 text-white/30 transition-[color,transform] duration-200 ease-out group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-white/70"
              />
            </CardTitle>
            <CardDescription>{description}</CardDescription>
          </CardHeader>
        </Card>
      ))}
    </div>
  );
}
```

### Variants

```tsx
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

const variants = [
  { variant: "default", title: "Default", description: "A raised surface with a faint lit edge. Use it for most content." },
  { variant: "glow", title: "Glow", description: "Neon border and ambient light. Save it for the one card that matters." },
  { variant: "outline", title: "Outline", description: "Border only, for dense layouts and nested groups." },
] as const;

export default function CardVariants() {
  return (
    <div className="grid w-full max-w-3xl gap-4 sm:grid-cols-3">
      {variants.map((item) => (
        <Card key={item.variant} variant={item.variant}>
          <CardHeader>
            <CardTitle>{item.title}</CardTitle>
            <CardDescription>{item.description}</CardDescription>
          </CardHeader>
        </Card>
      ))}
    </div>
  );
}
```

## API reference

### Card

Renders a div and accepts every native div attribute.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "glow" \| "outline"` | `"default"` | Surface style. |
| `color` | `NeonColor` | `inherited` | Palette name or hex. Falls back to --neon-accent, then cyan. |
| `interactive` | `boolean` | `false` | Hover lift, brighter edge and a focus ring when something inside is keyboard-focused. |

### CardHeader, CardContent, CardFooter

Divs with matching 24px padding. Content and footer drop their top padding so the slots stack evenly.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | — | Merged with the defaults, so p-4 or gap-3 override cleanly. |

### CardTitle, CardDescription

An h3 and a p. Swap the heading level with your own element if the outline needs it.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | — | Merged with the defaults. |

## Accessibility

- Card has no role or tab stop of its own; put the link or button inside it.
- For a clickable card, stretch one link over the surface with after:absolute after:inset-0 instead of wrapping the card in an anchor.
- The hover lift is disabled when the user prefers reduced motion.

## Source

`components/ui/card.tsx`

```tsx
import * as React from "react";

import { cn } from "@/lib/utils";
import { neonVars, type NeonColor } from "@/lib/neon";

const variants = {
  default: "border-white/10 bg-white/[0.03]",
  glow: "border-[rgb(var(--neon)/0.4)] bg-[rgb(var(--neon)/0.035)] shadow-[0_0_32px_-8px_rgb(var(--neon)/0.4),inset_0_0_24px_rgb(var(--neon)/0.05)]",
  outline: "border-white/[0.12] bg-transparent",
} as const;

const interactiveVariants = {
  default:
    "hover:border-white/[0.16] hover:bg-white/[0.045] hover:shadow-[0_16px_40px_-20px_rgb(var(--neon)/0.45)]",
  glow: "hover:border-[rgb(var(--neon)/0.6)] hover:shadow-[0_0_48px_-6px_rgb(var(--neon)/0.55),inset_0_0_28px_rgb(var(--neon)/0.08)]",
  outline: "hover:border-[rgb(var(--neon)/0.35)]",
} as const;

// The lit edge: a 1px line on the top border that fades out toward the corners.
const edges = {
  default: "bg-[linear-gradient(90deg,transparent,rgb(var(--neon)/0.5)_50%,transparent)] opacity-70",
  glow: "bg-[linear-gradient(90deg,transparent,rgb(var(--neon))_50%,transparent)] opacity-80",
  outline: null,
} as const;

export interface CardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "color"> {
  /** Palette name or any hex color. Inherits `--neon-accent` when omitted. */
  color?: NeonColor;
  variant?: keyof typeof variants;
  /** Lifts the card and brightens its light on hover. Pair with a link or button inside. */
  interactive?: boolean;
}

const Card = React.forwardRef<HTMLDivElement, CardProps>(
  ({ className, style, color, variant = "default", interactive = false, children, ...props }, ref) => {
    const edge = edges[variant];

    return (
      <div
        ref={ref}
        style={neonVars(color, style)}
        className={cn(
          "[--neon:var(--neon-accent,34_211_238)]",
          "group/card relative flex flex-col rounded-2xl border text-white/90",
          variants[variant],
          interactive && [
            "transition-[transform,border-color,background-color,box-shadow] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)]",
            "hover:-translate-y-0.5 motion-reduce:transition-none motion-reduce:hover:translate-y-0",
            "has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-[rgb(var(--neon)/0.6)] has-[:focus-visible]:ring-offset-2 has-[:focus-visible]:ring-offset-[#05060a]",
            interactiveVariants[variant],
          ],
          className,
        )}
        {...props}
      >
        {edge && (
          <span
            aria-hidden
            className={cn(
              "pointer-events-none absolute inset-x-0 -top-px h-px",
              edge,
              interactive && "transition-opacity duration-300 group-hover/card:opacity-100",
            )}
          />
        )}
        {children}
      </div>
    );
  },
);
Card.displayName = "Card";

const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn("flex flex-col gap-1.5 p-6", className)} {...props} />
  ),
);
CardHeader.displayName = "CardHeader";

const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
  ({ className, ...props }, ref) => (
    <h3
      ref={ref}
      className={cn("text-base font-semibold leading-tight tracking-[-0.01em] text-white/90", className)}
      {...props}
    />
  ),
);
CardTitle.displayName = "CardTitle";

const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
  ({ className, ...props }, ref) => (
    <p ref={ref} className={cn("text-sm leading-relaxed text-white/55", className)} {...props} />
  ),
);
CardDescription.displayName = "CardDescription";

const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
);
CardContent.displayName = "CardContent";

const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => (
    <div ref={ref} className={cn("mt-auto flex items-center gap-2 p-6 pt-0", className)} {...props} />
  ),
);
CardFooter.displayName = "CardFooter";

export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
```
