ephemera

Card

An index card, optionally ruled with a red header line.

Groceries

Saturday market

Bread, figs
Good olive oil
Flowers for Sam

Stationery box

Twelve cards, envelopes and a wax seal.

$24

Install

$ npx shadcn@latest add https://ephemera.kerrshift.com/r/card.json

Or copy the source below into components/ui/card.tsx. Core components need the Paper Theme and Radix where noted.

Usage

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

<Card ruled>
  <CardHeader>
    <CardTitle>Groceries</CardTitle>
    <CardDescription>Saturday market</CardDescription>
  </CardHeader>
  <CardContent>Bread, figs, good olive oil</CardContent>
  <CardFooter><Button size="sm">Done</Button></CardFooter>
</Card>

Props

PropTypeDefaultDescription
ruledbooleanfalseIndex-card lines: red under the header, blue in the content.
CardHeader, CardTitle, CardDescription, CardContent, CardFooterparts—Same structure as shadcn/ui, so it's a drop-in swap.

Any other props are passed to the root element.

Fonts

Components read optional CSS variables and fall back to system fonts: --font-display, --font-receipt, --font-type, --font-hand and --font-stamp. This site uses Fraunces, IBM Plex Mono, Courier Prime, Caveat and Anton.

Source

components/ui/card.tsx
import * as React from "react";

export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Index-card style: a red rule under the header and blue lines below. */
  ruled?: boolean;
}

/** An index card. */
export function Card({ ruled = false, className = "", ...props }: CardProps) {
  return (
    <div
      data-ruled={ruled || undefined}
      className={`paper group/card flex flex-col rounded-[3px] border border-(--edge) bg-(--paper-raised) text-(--ink) shadow-[0_1px_1px_rgb(0_0_0/.08),0_8px_18px_-8px_rgb(0_0_0/.28)] ${className}`}
      {...props}
    />
  );
}

export function CardHeader({ className = "", ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={`flex flex-col gap-1 px-6 pb-4 pt-5 group-data-[ruled]/card:border-b-2 group-data-[ruled]/card:border-(--margin) ${className}`}
      {...props}
    />
  );
}

export function CardTitle({ className = "", style, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
  return (
    <h3
      className={`text-[22px] font-semibold leading-tight tracking-tight ${className}`}
      style={{ fontFamily: "var(--font-display, Georgia, serif)", ...style }}
      {...props}
    />
  );
}

export function CardDescription({ className = "", ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
  return <p className={`text-[14px] leading-snug text-(--ink-muted) ${className}`} {...props} />;
}

export function CardContent({ className = "", style, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={`flex-1 px-6 py-4 group-data-[ruled]/card:bg-[repeating-linear-gradient(transparent_0_27px,var(--rule)_27px_28px)] group-data-[ruled]/card:leading-[28px] ${className}`}
      style={style}
      {...props}
    />
  );
}

export function CardFooter({ className = "", ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return <div className={`flex items-center gap-3 px-6 pb-5 pt-2 ${className}`} {...props} />;
}