FewTone UI
ComponentsDocs
  • Home

Getting started

  • Introduction
  • Installation
  • MCP Server
  • Smooth UI Skill
  • Roadmap

Components46

  • Accordion
  • Alert Dialog
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Calendar
  • Card
  • Checkbox
  • Combobox
  • Command
  • Context Menu
  • Copy Button
  • Date Picker
  • Dialog
  • Dropdown Menu
  • Hover Card
  • Input
  • Input OTP
  • Loading Button
  • Marquee
  • Navigation Menu
  • Number Input
  • Number Ticker
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Scroll Area
  • Select
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Spinner
  • Stepper
  • Switch
  • Switcher
  • Table
  • Tabs
  • Text Effects
  • Textarea
  • Theme Toggle
  • Toast
  • Toggle Group
  • Tooltip
© 2026 FewTone · MIT licenceGitHub
  1. Components
  2. Card

Card

pnpm dlx shadcn@latest add …/r/card.json

A React card for Next.js and Tailwind CSS: a surface for grouped content with header, action, content and footer parts, a small size and a link style.

  • Create project

    Deploy a new project in a few clicks.
    Default
  • Team plan

    8 of 10 seats used
    Active
    Small, with an action
  • Source on GitHub

    Browse the code and open issues.
    Link
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"

export function CreateProject() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Create project</CardTitle>
        <CardDescription>Deploy a new project in a few clicks.</CardDescription>
      </CardHeader>
      <CardContent>{/* fields */}</CardContent>
      <CardFooter>
        <Button>Deploy</Button>
      </CardFooter>
    </Card>
  )
}
overview.tsx
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { ArrowUpRightIcon } from "lucide-react";
import { useId } from "react";

/*
 * The range at a glance, each named underneath: a form card with every part, a small card with an action, and a
 * whole card as a link (hover it: the surface lifts a step).
 */
export default function CardOverview() {
  const id = useId();

  return (
    <ul className="grid w-full max-w-3xl items-start gap-x-6 gap-y-8 md:grid-cols-2">
      <li className="flex flex-col items-center gap-2 md:row-span-2">
        <Card className="w-full">
          <CardHeader>
            <CardTitle asChild>
              <h3>Create project</h3>
            </CardTitle>
            <CardDescription>Deploy a new project in a few clicks.</CardDescription>
          </CardHeader>
          <CardContent className="grid gap-4">
            <div className="grid gap-2">
              <label htmlFor={`${id}-name`} className="text-sm font-medium">
                Name
              </label>
              <Input id={`${id}-name`} placeholder="My project" />
            </div>
            <div className="grid gap-2">
              <label htmlFor={`${id}-repo`} className="text-sm font-medium">
                Repository
              </label>
              <Input id={`${id}-repo`} placeholder="github.com/you/project" />
            </div>
          </CardContent>
          <CardFooter className="justify-end">
            <Button variant="outline">Cancel</Button>
            <Button>Deploy</Button>
          </CardFooter>
        </Card>
        <span className="text-xs text-muted-foreground">Default</span>
      </li>

      <li className="flex flex-col items-center gap-2">
        <Card size="sm" className="w-full">
          <CardHeader>
            <CardTitle asChild>
              <h3>Team plan</h3>
            </CardTitle>
            <CardDescription>8 of 10 seats used</CardDescription>
            <CardAction>
              <Badge variant="secondary">Active</Badge>
            </CardAction>
          </CardHeader>
          <CardFooter>
            <Button variant="outline" size="sm">
              Manage seats
            </Button>
          </CardFooter>
        </Card>
        <span className="text-xs text-muted-foreground">Small, with an action</span>
      </li>

      <li className="flex flex-col items-center gap-2">
        <Card asChild size="sm" className="w-full">
          <a href="https://github.com/FewTone/ui" target="_blank" rel="noreferrer">
            <CardHeader>
              <CardTitle asChild>
                <h3>Source on GitHub</h3>
              </CardTitle>
              <CardDescription>Browse the code and open issues.</CardDescription>
              <CardAction>
                <ArrowUpRightIcon aria-hidden className="size-(--size-icon-md) text-muted-foreground" />
              </CardAction>
            </CardHeader>
          </a>
        </Card>
        <span className="text-xs text-muted-foreground">Link</span>
      </li>
    </ul>
  );
}

Installation

  1. 1

    Install the dependencies.

    Package manager
    $ pnpm add class-variance-authority radix-ui
  2. 2

    Add the FewTone UI tokens to your global CSS file. Skip any variable your project already defines.

    app/globals.css
    :root {
      --size-control-sm: 2rem;
      --size-control-md: 2.25rem;
      --size-control-lg: 2.5rem;
      --size-icon-sm: 0.875rem;
      --size-icon-md: 1rem;
      --size-icon-lg: 1.25rem;
      --size-roll-fade: 0.25em;
      --size-roll-fade-x: 0.5em;
      --duration-instant: 80ms;
      --duration-fast: 150ms;
      --duration-base: 220ms;
      --duration-slow: 350ms;
      --duration-slower: 500ms;
      --z-overlay: 1200;
      --z-modal: 1300;
      --z-popover: 1400;
      --z-toast: 1500;
      --z-tooltip: 1600;
      --focus-ring-width: 2px;
      --focus-ring-offset: 2px;
      --opacity-disabled: 0.5;
      --ease-spring: linear(0, 0.157, 0.48, 0.77, 0.97, 1.07, 1.1, 1.09, 1.05, 1.02, 1, 0.995, 1);
      --destructive-foreground: oklch(1 0 0);
      --overlay: color-mix(in oklch, oklch(0 0 0) 40%, transparent);
    }
    
    .dark {
      --destructive-foreground: oklch(1 0 0);
      --overlay: color-mix(in oklch, oklch(0 0 0) 60%, transparent);
    }
    
    @theme inline {
      --animate-popover-in: popover-in var(--duration-fast) cubic-bezier(0.22, 1, 0.36, 1) both;
      --animate-popover-out: popover-out var(--duration-instant) cubic-bezier(0.55, 0, 1, 0.45) both;
      --animate-collapsible-open: collapsible-open var(--duration-slow) cubic-bezier(0.22, 1, 0.36, 1);
      --animate-collapsible-close: collapsible-close var(--duration-base) cubic-bezier(0.55, 0, 1, 0.45) both;
      --color-destructive-foreground: var(--destructive-foreground);
      --color-overlay: var(--overlay);
      @keyframes popover-in {
        from {
          opacity: 0;
          filter: blur(6px);
          transform: scale(0.96);
        }
        to {
          opacity: 1;
          filter: blur(0);
          transform: scale(1);
        }
      }
      @keyframes popover-out {
        from {
          opacity: 1;
          filter: blur(0);
          transform: scale(1);
        }
        to {
          opacity: 0;
          filter: blur(6px);
          transform: scale(0.96);
        }
      }
      @keyframes collapsible-open {
        from {
          height: 0;
          opacity: 0;
          filter: blur(6px);
        }
        to {
          height: var(--collapsible-height);
          opacity: 1;
          filter: blur(0);
        }
      }
      @keyframes collapsible-close {
        from {
          height: var(--collapsible-height);
          opacity: 1;
          filter: blur(0);
        }
        to {
          height: 0;
          opacity: 0;
          filter: blur(6px);
        }
      }
    }
    
    @media (prefers-reduced-motion: reduce) {
      :root {
        --duration-instant: 1ms;
        --duration-fast: 1ms;
        --duration-base: 1ms;
        --duration-slow: 1ms;
        --duration-slower: 1ms;
      }
    }
  3. 3

    Copy the source into your project.

    components/ui/card.tsx
    import { cn } from "@/lib/utils";
    import { cva, type VariantProps } from "class-variance-authority";
    import { Slot } from "radix-ui";
    import type { ComponentProps } from "react";
    
    /*
     * A surface for grouped content (owner's roadmap, 2026-10-16), with shadcn's parts: Card, CardHeader, CardTitle,
     * CardDescription, CardAction, CardContent and CardFooter.
     * - Depth comes from a soft shadow and a hairline, not a border (design.md: few borders), the recipe of the site's
     *   own cards. The hairline is a ring, outside the box, so it never changes the layout.
     * - As a link (`asChild` with an `a`), the surface lifts a step on hover, to `muted`: colour only, nothing moves.
     *   The whole card is the link, with the focus ring round it, as Badge and Button do as links.
     * - `size="sm"` tightens the padding and gaps from 24 to 16px, for dense grids such as stats and settings.
     * - The header is a grid. CardAction takes a second column beside the title and description; that column is
     *   implicit, so it sizes itself to the action without an arbitrary grid template.
     * - No motion and no "use client": it's a server component.
     */
    
    const cardVariants = cva(
      [
        "group/card flex flex-col rounded-lg bg-card text-card-foreground shadow-sm ring-1 ring-border",
        "outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)",
        // A link card lifts a step on hover: the background only
        "[a&]:transition-colors [a&]:duration-(--duration-fast) [a&]:ease-out [a&]:hover:bg-muted",
      ],
      {
        variants: {
          size: {
            sm: "gap-4 py-4",
            md: "gap-6 py-6",
          },
        },
        defaultVariants: { size: "md" },
      },
    );
    
    export type CardProps = ComponentProps<"div"> &
      VariantProps<typeof cardVariants> & {
        /** Render the single child instead of a div, for example a link, keeping the card styles */
        asChild?: boolean;
      };
    
    /**
     * A surface for grouped content. It renders a `div`, or with `asChild` its single child, so a link can be a whole
     * card. Keep a link card short: everything inside it is read as the link's name.
     */
    function Card({ className, size, asChild = false, ...props }: CardProps) {
      const Comp = asChild ? Slot.Root : "div";
      return (
        <Comp
          data-slot="card"
          data-size={size ?? "md"}
          className={cn(cardVariants({ size }), className)}
          {...props}
        />
      );
    }
    
    function CardHeader({ className, ...props }: ComponentProps<"div">) {
      return (
        <div
          data-slot="card-header"
          className={cn(
            "grid auto-rows-min grid-cols-1 items-start gap-x-4 gap-y-1.5 px-6 group-data-[size=sm]/card:px-4",
            // With a rule under it (className="border-b"), the header keeps the card's padding below the rule
            "[.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
            className,
          )}
          {...props}
        />
      );
    }
    
    /** The card's title. A `div` by default; with `asChild`, a heading at the level the page needs (`<h3>`). */
    function CardTitle({ className, asChild = false, ...props }: ComponentProps<"div"> & { asChild?: boolean }) {
      const Comp = asChild ? Slot.Root : "div";
      return <Comp data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />;
    }
    
    function CardDescription({ className, ...props }: ComponentProps<"div">) {
      return (
        <div data-slot="card-description" className={cn("text-sm text-muted-foreground", className)} {...props} />
      );
    }
    
    /** Sits at the end of the header, beside the title and description: a button, a badge, a menu */
    function CardAction({ className, ...props }: 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 CardContent({ className, ...props }: ComponentProps<"div">) {
      return (
        <div
          data-slot="card-content"
          className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
          {...props}
        />
      );
    }
    
    function CardFooter({ className, ...props }: ComponentProps<"div">) {
      return (
        <div
          data-slot="card-footer"
          className={cn(
            "flex items-center gap-2 px-6 group-data-[size=sm]/card:px-4",
            "[.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
            className,
          )}
          {...props}
        />
      );
    }
    
    export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cardVariants };
  4. 4

    Update the import paths to match your project.

API reference

Card

The surface: a soft shadow and a hairline, no border. Also accepts every native div prop. Exports cardVariants for styling other elements.

PropTypeDefaultDescription
size?"sm" | "md""md"Padding and gaps: 24px, or 16px for dense grids.
asChild?booleanfalseRender the single child (for example a link) with the card styles. As a link, the surface lifts a step on hover; everything inside is read as the link's name, so keep it short.

CardHeader

Holds the title, description and action. Add className="border-b" for a rule under it; the padding below follows.

PropTypeDefaultDescription

CardTitle

PropTypeDefaultDescription
asChild?booleanfalseRender a heading at the level your page needs instead of a div, for example <h3>.

CardDescription

Muted text under the title.

PropTypeDefaultDescription

CardAction

Sits at the end of the header, beside the title and description: a button, a badge, a menu.

PropTypeDefaultDescription

CardContent

The body, with the card's side padding.

PropTypeDefaultDescription

CardFooter

A row for actions, 8px apart. Add className="border-t" for a rule above it; the padding above follows.

PropTypeDefaultDescription

Related components

  • Button

    An accessible React button for Next.js and Tailwind CSS with six variants, three sizes, icons and a soft tint on hover and press, built on a native button.

  • Stablev2.4.0Beta
    Badge

    An accessible React badge for Next.js and Tailwind CSS: small status labels in five variants, including one with a light travelling round its edge.

  • OMJLIN+3
    Avatar

    An accessible React avatar for Next.js and Tailwind CSS: a picture or initials in three sizes, alone or in an overlapping group, built on Radix Avatar.