Skip to content
Ballpoint
Contents

Card

A box pencilled around a block of content, with a ruled-off footer.

Ink refill
Medium point, blue. Writes about two kilometres.
In stock

Fits most clicky pens. Comes in a paper sleeve, not plastic.

Rounded, smaller
size="sm" with a 12px radius.

Cards take the same pen settings as everything else.

Installation

pnpm dlx shadcn@latest add @ballpoint/card
Or copy the source
ui/card.tsx
"use client";

import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
import { usePen, type Pen } from "@/hooks/use-ink-box";
import { InkOutline } from "@/lib/ink-outline";
import { Separator } from "@/components/ui/separator";

/**
 * A box pencilled around a block of content. The outline is decoration, so
 * it's drawn at a lighter pressure than a control's.
 */
function Card({
  className,
  size = "default",
  children,
  seed,
  roughness,
  passes,
  radius,
  corners,
  draw,
  weight,
  speed,
  ...props
}: ComponentProps<"div"> & Omit<Pen, "fill" | "shadow"> & { size?: "default" | "sm"; seed?: string | number }) {
  const pen = usePen({ roughness, passes, radius, corners, draw, weight, speed });
  return (
    <div
      data-slot="card"
      data-size={size}
      className={cn(
        "group/card relative flex flex-col gap-(--card-spacing) py-(--card-spacing) text-base text-card-foreground [--card-spacing:--spacing(6)] has-data-[slot=card-footer]:pb-0 data-[size=sm]:[--card-spacing:--spacing(4)]",
        className,
      )}
      {...props}
    >
      <InkOutline pen={pen} seed={seed} estimate={[360, 220]} maxRadius={18} className="text-ink-line" />
      {children}
    </div>
  );
}

function CardHeader({ className, ...props }: ComponentProps<"div">) {
  return (
    <div
      data-slot="card-header"
      className={cn(
        "group/card-header @container/card-header relative grid auto-rows-min items-start gap-1 px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
        className,
      )}
      {...props}
    />
  );
}

function CardTitle({ className, ...props }: ComponentProps<"div">) {
  return <div data-slot="card-title" className={cn("text-xl leading-snug font-bold group-data-[size=sm]/card:text-lg", className)} {...props} />;
}

function CardDescription({ className, ...props }: ComponentProps<"div">) {
  return <div data-slot="card-description" className={cn("text-sm text-ink-3", className)} {...props} />;
}

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("relative px-(--card-spacing)", className)} {...props} />;
}

/** The footer is ruled off with a pen line rather than tinted. */
function CardFooter({ className, children, ...props }: ComponentProps<"div">) {
  return (
    <div data-slot="card-footer" className={cn("relative flex flex-col", className)} {...props}>
      <div className="px-(--card-spacing)">
        <Separator />
      </div>
      <div className="flex items-center gap-3 p-(--card-spacing)">{children}</div>
    </div>
  );
}

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

Usage

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

<Card>
  <CardHeader>
    <CardTitle>Ink refill</CardTitle>
    <CardDescription>Medium point, blue.</CardDescription>
  </CardHeader>
  <CardContent>Fits most clicky pens.</CardContent>
  <CardFooter>…</CardFooter>
</Card>

API reference

size"default" | "sm"default "default"
Inner spacing.
seedstring | number
Pins the drawing.

Pen settings

The drawing also takes draw, roughness, passes, radius, corners, weight and speed, as props or from the nearest InkProvider. What each one does.