ephemera

Paper Theme

Six paper types as CSS variables (Cream, Kraft, Ledger, Blueprint, Newsprint, Carbon), merged into your global CSS, plus the five Ephemera fonts. Also re-skins existing shadcn/ui components.

New letter

Goes out with the morning post.

  • Sam Okafor
    Postcard from Lisbon
    Travel
  • Studio Mori
    Invoice Nº 0042
    Due
  • Grandma
    The fig jam recipe
    Keep

Install

$ npx shadcn@latest add https://ephemera.kerrshift.com/r/paper-theme.json

Or copy the source below into styles/paper.css. Then import it in your global CSS.

Usage

setup
# installs the theme (merged into your global CSS) and its five fonts
npx shadcn@latest add https://ephemera.kerrshift.com/r/paper-theme.json

/* app/layout.tsx: pick a paper */
<html data-paper="cream">  {/* cream | kraft | ledger | blueprint | newsprint | carbon */}

/* any element can switch paper */
<section data-paper="blueprint" className="paper">…</section>

The theme also sets the standard shadcn/ui variables (--background, --primary, --border…), so components you already have pick up the paper colors too. The shadcn CLI merges the theme straight into your global CSS and installs the fonts (next/font in Next.js, @fontsource packages elsewhere), so there's nothing to import by hand. The source stylesheet is shown below.

Props

PropTypeDefaultDescription
data-paper"cream" | "kraft" | "ledger" | "blueprint" | "newsprint" | "carbon""cream"Paper type for that element and everything inside it. Nest them freely.
.paperclass—Paper color, ink color and a fine grain texture.
.paper-ruledclass—Ruled lines with a red margin. Set --line to change the spacing.
--paper, --paper-raised, --paper-sunkcolor—Page, cards, and recessed areas.
--ink, --ink-muted, --ink-faintcolor—Text, secondary text, placeholders.
--pen, --pen-inkcolor—The accent ink and the text that sits on it.
--edge, --rule, --margin, --dangercolor—Borders, ruled lines, margin line, destructive ink.

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

styles/paper.css
/*
 * Ephemera paper theme.
 * Put data-paper="cream | kraft | ledger | blueprint | newsprint | carbon" on <html> or any container.
 * Also maps the standard shadcn/ui variables, so existing shadcn components pick up the paper colors.
 */

[data-paper],
[data-paper="cream"] {
  --paper: #fbf8f1;
  --paper-raised: #fffdf8;
  --paper-sunk: #efe9dc;
  --ink: #24221d;
  --ink-muted: #6f685c;
  --ink-faint: #a8a091;
  --edge: #ddd3c1;
  --rule: #c3d4e8;
  --margin: #e6a5a5;
  --pen: #1f3c88;
  --pen-ink: #fbf8f1;
  --danger: #b3261e;
  --grain-alpha: 0.07;
}

[data-paper="kraft"] {
  --paper: #d6bf98;
  --paper-raised: #e3cfab;
  --paper-sunk: #c7ad82;
  --ink: #2b2016;
  --ink-muted: #5f4b35;
  --ink-faint: #8f7756;
  --edge: #b89b6e;
  --rule: #b69a6c;
  --margin: #a84a3c;
  --pen: #8f2d22;
  --pen-ink: #f3e6cd;
  --danger: #8f2d22;
  --grain-alpha: 0.13;
}

[data-paper="ledger"] {
  --paper: #e8efd9;
  --paper-raised: #f1f5e6;
  --paper-sunk: #dce6c9;
  --ink: #1d2a1f;
  --ink-muted: #556451;
  --ink-faint: #8b9b84;
  --edge: #bfd0a8;
  --rule: #a9c69a;
  --margin: #d98b8b;
  --pen: #1f5a36;
  --pen-ink: #f1f5e6;
  --danger: #a3302a;
  --grain-alpha: 0.06;
}

[data-paper="blueprint"] {
  --paper: #1f4f8f;
  --paper-raised: #25599c;
  --paper-sunk: #1a457e;
  --ink: #eef4ff;
  --ink-muted: #b4cbec;
  --ink-faint: #7f9fcb;
  --edge: #4a76b3;
  --rule: rgb(255 255 255 / 0.16);
  --margin: rgb(255 255 255 / 0.35);
  --pen: #f4f8ff;
  --pen-ink: #1f4f8f;
  --danger: #ffb4a8;
  --grain-alpha: 0.08;
}

[data-paper="newsprint"] {
  --paper: #ebe9e1;
  --paper-raised: #f3f1ea;
  --paper-sunk: #dedbd0;
  --ink: #141413;
  --ink-muted: #55534d;
  --ink-faint: #8d8a81;
  --edge: #c9c5b8;
  --rule: #cfcbbf;
  --margin: #b9b4a6;
  --pen: #141413;
  --pen-ink: #ebe9e1;
  --danger: #a3241c;
  --grain-alpha: 0.11;
}

[data-paper="carbon"] {
  --paper: #1c1b19;
  --paper-raised: #252421;
  --paper-sunk: #151412;
  --ink: #ece6d9;
  --ink-muted: #a39b8c;
  --ink-faint: #6e685e;
  --edge: #3a3732;
  --rule: #34312c;
  --margin: #6d3a36;
  --pen: #e9cf8f;
  --pen-ink: #1c1b19;
  --danger: #f08a7e;
  --grain-alpha: 0.1;
}

/* shadcn/ui bridge */
[data-paper] {
  --background: var(--paper);
  --foreground: var(--ink);
  --card: var(--paper-raised);
  --card-foreground: var(--ink);
  --popover: var(--paper-raised);
  --popover-foreground: var(--ink);
  --primary: var(--pen);
  --primary-foreground: var(--pen-ink);
  --secondary: var(--paper-sunk);
  --secondary-foreground: var(--ink);
  --muted: var(--paper-sunk);
  --muted-foreground: var(--ink-muted);
  --accent: var(--paper-sunk);
  --accent-foreground: var(--ink);
  --destructive: var(--danger);
  --border: var(--edge);
  --input: var(--edge);
  --ring: var(--pen);
  --radius: 0.25rem;

  --grain: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}

/* Layered so Tailwind utilities (fixed, absolute, bg-*) can override them. */
@layer components {
  /* A sheet of paper: color, ink and grain. */
  .paper {
    position: relative;
    isolation: isolate;
    background-color: var(--paper);
    color: var(--ink);
  }
  .paper::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-image: var(--grain);
    opacity: var(--grain-alpha);
    mix-blend-mode: multiply;
    border-radius: inherit;
  }

  /* Ruled lines with a margin. Set --line to change the spacing. */
  .paper-ruled {
    --line: 28px;
    background-image:
      linear-gradient(90deg, transparent 2.25rem, var(--margin) 2.25rem, var(--margin) calc(2.25rem + 1px), transparent calc(2.25rem + 1px)),
      repeating-linear-gradient(transparent 0 calc(var(--line) - 1px), var(--rule) calc(var(--line) - 1px) var(--line));
    background-attachment: local;
  }
}

@keyframes paper-letter-in {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 48px)) rotate(-2.5deg); }
  to { opacity: 1; transform: translate(-50%, -50%) rotate(0deg); }
}
@keyframes paper-letter-out {
  from { opacity: 1; transform: translate(-50%, -50%); }
  to { opacity: 0; transform: translate(-50%, calc(-50% + 32px)) rotate(1.5deg); }
}
@keyframes paper-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes paper-fade-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes paper-print {
  from { clip-path: inset(0 0 100% 0); transform: translateY(-14px); }
  to { clip-path: inset(0 0 0 0); transform: translateY(0); }
}
@keyframes paper-tear-away {
  to { opacity: 0; transform: translate(40px, 30px) rotate(9deg); }
}
@keyframes paper-tick {
  from { stroke-dashoffset: 30; }
  to { stroke-dashoffset: 0; }
}

@media (prefers-reduced-motion: reduce) {
  [data-paper-motion] { animation-duration: 1ms !important; }
}

@keyframes paper-pop-in {
  from { opacity: 0; transform: translateY(-4px) scale(0.98); }
  to { opacity: 1; transform: none; }
}
@keyframes paper-blot {
  0% { transform: scale(0.2); opacity: 0.4; }
  70% { transform: scale(1.12); opacity: 1; }
  100% { transform: scale(1); }
}
@keyframes paper-accordion-down {
  from { height: 0; }
  to { height: var(--radix-accordion-content-height); }
}
@keyframes paper-accordion-up {
  from { height: var(--radix-accordion-content-height); }
  to { height: 0; }
}
@keyframes paper-sketch {
  to { background-position: 24px 0; }
}
@keyframes paper-draw {
  from { stroke-dashoffset: 120; }
  to { stroke-dashoffset: 0; }
}
@keyframes paper-slide-in-right { from { transform: translateX(100%); } to { transform: none; } }
@keyframes paper-slide-out-right { from { transform: none; } to { transform: translateX(100%); } }
@keyframes paper-slide-in-left { from { transform: translateX(-100%); } to { transform: none; } }
@keyframes paper-slide-out-left { from { transform: none; } to { transform: translateX(-100%); } }
@keyframes paper-slide-in-bottom { from { transform: translateY(100%); } to { transform: none; } }
@keyframes paper-slide-out-bottom { from { transform: none; } to { transform: translateY(100%); } }