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.
- TravelOct 12Sam OkaforPostcard from Lisbon
- DueOct 11Studio MoriInvoice Nº 0042
- KeepOct 9GrandmaThe fig jam recipe
Install
$ npx shadcn@latest add https://ephemera.kerrshift.com/r/paper-theme.jsonOr copy the source below into styles/paper.css. Then import it in your global CSS.
Usage
# 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
| Prop | Type | Default | Description |
|---|---|---|---|
| data-paper | "cream" | "kraft" | "ledger" | "blueprint" | "newsprint" | "carbon" | "cream" | Paper type for that element and everything inside it. Nest them freely. |
| .paper | class | — | Paper color, ink color and a fine grain texture. |
| .paper-ruled | class | — | Ruled lines with a red margin. Set --line to change the spacing. |
| --paper, --paper-raised, --paper-sunk | color | — | Page, cards, and recessed areas. |
| --ink, --ink-muted, --ink-faint | color | — | Text, secondary text, placeholders. |
| --pen, --pen-ink | color | — | The accent ink and the text that sits on it. |
| --edge, --rule, --margin, --danger | color | — | 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
/*
* 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%); } }