← Deckle THE GUIDE · HOW IT WAS BUILT
Design showcase · build notes

The whole site is one sheet.

Deckle is a light-first Astro site for a fictional paper mill. Its two signature moves are the same idea from two directions: the page itself is a sheet of paper you can re-rule — blank, dot, ruled, graph — and a scratch-pad canvas where line weight follows the speed of your hand, like a wet nib on rag.

The idea

A stationery brand sells the feeling of good paper: tooth, weight, a ragged edge, ink that sits up instead of feathering. Screens have none of that, so Deckle fakes the physics honestly. The background is a fixed “substrate” carrying the current ruling; the hero notebook, the doodle pad and the substrate all read the same CSS custom properties, so flipping the ruling re-rules everything at once — hero and all. Kraft is the daylight theme (#ece3d1), ink-blue the after-hours one (#14171f), and the only loud colour on the site is a highlighter (#f2c14e) used exactly the way a highlighter is: in swipes.

The stack — why Astro

Signature technique #1 — re-ruling the page with :has()

The paper toggle is a plain radio group. No JavaScript swaps the pattern — a body:has() selector watches which radio is checked and rewrites two custom properties; everything with the ruling (the fixed substrate, the hero notebook, the doodle pad) just inherits them:

body:has(#pm-dot:checked) {
  --paper-img: radial-gradient(var(--rule) 1px, transparent 1.4px);
  --paper-size: 22px 22px;
}
body:has(#pm-ruled:checked) {
  --paper-img: linear-gradient(to bottom,
      transparent calc(100% - 1px), var(--rule) calc(100% - 1px));
  --paper-size: 100% 2rem;
}

.paper, .substrate {
  background-image: var(--paper-img);
  background-size: var(--paper-size);
}

/* the red margin only exists when the sheet is ruled */
body:has(#pm-ruled:checked) .margin-line { opacity: .75; }

Because it's CSS, the swap works with JavaScript disabled and under prefers-reduced-motion — a swap isn't motion. The ruling colour is color-mix(in oklab, var(--ink) 11%, transparent), so it re-tints itself when the theme flips. JS's only job is remembering your choice in localStorage.

Signature technique #2 — a nib that answers your hand

The scratch pad has no pressure API to lean on, so it uses the oldest proxy there is: speed. A slow hand lets ink pool; a fast one starves the line. Each pointermove measures velocity in px/ms and eases the stroke width toward an exponential of it:

function nibWidth(v) {            // v — pointer speed, px/ms
  var target = 1 + (6.5 - 1) * Math.exp(-v / 0.55);
  lastW += (target - lastW) * 0.3; // the swell, not the jitter
  return lastW;
}

pad.addEventListener('pointermove', function (e) {
  var dist = Math.hypot(p.x - lastPt.x, p.y - lastPt.y);
  if (dist < 1.4) return;         // ignore micro-tremor
  var v = dist / Math.max(1, e.timeStamp - lastT);
  cur.pts.push({ x: p.x, y: p.y, w: nibWidth(v) });
  segment(...);                    // quadratic through midpoints
});

Strokes are stored as point lists, not pixels, so the pad survives a resize and even a theme toggle — flip to dark and your doodle redraws in kraft-coloured ink. There is no idle animation loop: the canvas only paints on input, and the DPR is capped at 1.5. When the desk scrolls into view a sample flourish writes itself — its points are spaced unevenly, so the same nib model gives it genuine thick-and-thin. Under reduced motion that flourish renders as one static frame, drawing itself in exactly zero milliseconds.

Details that matter

Ship it on GitHub Pages

Astro builds static HTML, so Pages serves it straight from the repo — no Actions workflow. Two config lines do the work:

// astro.config.mjs
export default defineConfig({
  site: 'https://bswxyz.github.io',
  base: '/deckle',      // project-site path prefix
  outDir: './docs',     // Pages serves main + /docs
});

Every internal link goes through import.meta.env.BASE_URL so nothing 404s under the /deckle/ prefix, a .nojekyll in public/ keeps Pages from mangling the output, and deploying is:

npm run build                      # emits ./docs
git add -A && git commit -m "first pull off the mould"
gh repo create bswxyz/deckle --public --source . --push
gh api --method POST /repos/bswxyz/deckle/pages \
  -f 'source[branch]=main' -f 'source[path]=/docs'

Deckle is a design-showcase concept — the mill, the prices and the Thursday post van are fiction, and the till sends nothing. The repository README maps what a production version would need.

← Back to the mill