Card

A hand-drawn card container styled with shadcn conventions. The card body can carry a paper texture behind its content.

Card
Every hand-drawn card composition and size.
Sizes
Default size
Comfortable spacing for most content.

Sketchy borders drawn at the default scale.

Small size
Tighter spacing for dense layouts.

Sketchy borders drawn at the small scale.

With action
Sketch #12
Last edited a moment ago.

An action sits beside the header content.

With footer
Publish sketch
Share it with the rest of the team.

A footer anchors actions to the bottom of the card.

Paper texturesPatterns from Hero Patterns
Paper
Flecked, handmade paper grain.
Polka dots
Bullet-journal dot paper.
Hexagons
Honeycomb hexagon grid.
Graph
Engineering graph paper.
Plus
Scattered plus-sign paper.
Filled dots
Fine speckled dot paper.

Installation

npx shadcn@latest add https://sketchcn.chuwii.com/r/card.json

Styles

Card leans on these rules from sketch.css. The shadcn CLI installs the file with sketch-provider; copying the component by hand means copying them too.

[data-paper]:not([data-paper="default"])::before,
[data-slot="card"][data-variant]:not([data-variant="default"])::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--paper-pattern);
  opacity: var(--paper-opacity, 0.4);
  mask-image: var(--paper-image);
  -webkit-mask-image: var(--paper-image);
  mask-repeat: repeat;
  -webkit-mask-repeat: repeat;
}

Read the whole file for the drawing variables, paper textures and reduced-motion rules.

Usage

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

export function SketchCard() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Sketch card</CardTitle>
        <CardDescription>Rough edges on purpose.</CardDescription>
      </CardHeader>
      <CardContent>Every component ships as plain source you own.</CardContent>
    </Card>
  );
}

Parts

PartDescription
CardThe outlined container.
CardHeaderTitle, description and action row.
CardTitleHeading text, scales down at size="sm".
CardDescriptionMuted supporting text.
CardActionPinned to the top right of the header.
CardContentMain body.
CardFooterBordered footer strip with a muted background.

Props

PropTypeDefaultDescription
size"default" | "sm""default"Tightens the --card-spacing and the title size.
variant"default" | "polkadots" | "hexagons" | "graph" | "plus" | "filled-dots""default"Paper texture drawn behind the content.

All other props are forwarded to the underlying div.

Paper variants

<Card variant="graph">
  <CardHeader>
    <CardTitle>Graph paper</CardTitle>
    <CardDescription>Engineering graph paper.</CardDescription>
  </CardHeader>
</Card>
VariantLook
defaultPlain card surface.
polkadotsBullet-journal dot paper.
hexagonsHex grid.
graphEngineering graph paper.
plusScattered plus-sign paper.
filled-dotsFine speckled dot paper.

Examples

Sign-in card

Welcome back
Sign in to keep sketching.
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";

export function SignInCard() {
  return (
    <Card variant="graph" className="w-full max-w-sm">
      <CardHeader>
        <CardTitle>Welcome back</CardTitle>
        <CardDescription>Sign in to keep sketching.</CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-3">
        <Input type="email" placeholder="[email protected]" />
        <Input type="password" placeholder="Password" />
      </CardContent>
      <CardFooter>
        <Button className="w-full">Sign in</Button>
      </CardFooter>
    </Card>
  );
}

Settings card

Notifications
Choose what lands in your inbox.
Weekly digests only, never more than one per week.
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";

export function NotificationsCard() {
  const id = useId();

  return (
    <Card size="sm" className="w-full max-w-sm">
      <CardHeader>
        <CardTitle>Notifications</CardTitle>
        <CardDescription>Choose what lands in your inbox.</CardDescription>
        <CardAction>
          <Switch id={id} defaultChecked aria-label="Notifications" />
        </CardAction>
      </CardHeader>
      <CardContent className="text-muted-foreground text-sm">
        Weekly digests only, never more than one per week.
      </CardContent>
    </Card>
  );
}
Button
A hand-drawn Base UI button.
import { Link } from "@tanstack/react-router";
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

export function ComponentLinkCard() {
  return (
    <Link to="/components/$name" params={{ name: "button" }}>
      <Card className="h-full transition-transform hover:-translate-y-1">
        <CardHeader>
          <CardTitle className="flex items-center justify-between gap-2">
            Button
            <ArrowRight className="size-4" />
          </CardTitle>
          <CardDescription>A hand-drawn Base UI button.</CardDescription>
        </CardHeader>
      </Card>
    </Link>
  );
}