skip to content
lav.ren / tinker / paper-materiality

Paper on paper

Making bento cards feel physically present on a paper surface — from floating drop shadows to contact shadows, texture matching, and SVG displacement filters.

Date
2026
Stack
ReactSVGCSS

The homepage is a bento grid of paper cards on a photographed paper background. The cards need to feel like they are physically resting on that surface — not hovering above it. A standard box-shadow immediately creates the wrong metaphor: distance between object and ground. Paper on a desk has almost zero lift. The shadow is a contact phenomenon, not a drop.

This page walks through the techniques explored to make the cards feel present, what failed, and the final recipe that shipped.

§ hello

Andrei Lavrenov — full-stack developer.

The shadow problem

A drop shadow says “this thing is floating a few millimeters off the surface.” The larger the blur and offset, the higher the perceived lift. That is exactly wrong for paper on a desk.

A contact shadow inverts the cues. The blur is tiny (1–2 px), the offset is nearly zero, and there is a hairline solid shadow at 0.5 px that reads as the paper physically touching the surface. The eye registers it as weight, not distance. Toggle between the three modes above — the contact version does not look dramatic, but it reads as resting.

What did not work: directional shadows with an X-offset. They looked like a dark underglow bleeding from one side of the card, not like a ground-contact phenomenon. The original bento cards had exactly this — a 3 px horizontal offset plus a second 1 px “contact” layer. It read as a sharp, visible edge rather than physical presence, and disappeared inconsistently when the postit mode toggled because the two shadow recipes were structurally different.

§ hello

Andrei Lavrenov — full-stack developer.

Texture changes everything

The contact shadow got the grounding right, but the card still felt like a flat digital rectangle sitting on a photograph. The problem was materiality mismatch: the background has visible fibers, grain, and specks. The card was a solid #faf6ee slab.

Adding the same paper texture to the card — offset so the grain does not align with the background — was the breakthrough. The card immediately reads as “a different sheet of the same paper stock.” background-blend-mode: luminosity lets the card carry a slightly warmer tint (#fcf8f0) without the texture overpowering the content. The inset 0.5 px ring completes the picture: it reads as the visible edge of a cut piece of paper.

The offset is important. If the card texture aligns pixel-for-pixel with the background, the boundary vanishes — the card disappears into the surface. A random background-position per card (same pattern as the existing postit tilt) ensures every card has unique grain.

Shell
3.0
0.040
Outline
5.0
0.050
1.5px
0.090
§ hello

Andrei Lavrenov — full-stack developer.

Fiber edges

Real paper has fibers at its cut edges. Digital cards have pixel-perfect rectangles. SVG displacement filters can warp a rendered boundary to look organic — and this site already uses them for the ink-bleed effect on text.

The catch: applying filter: url(#displacement) to the card element warps everything inside it, including text. The solution is to filter a pseudo-element instead of the card itself.

Two layers sit around the card:

  1. A backdrop shell (::before in CSS, a positioned div in React) duplicates the card’s paper background and takes a displacement filter. It sits behind the card content at z-index 0. The warped edges poke out past the crisp card boundary as fibrous fringe.

  2. A displaced outline (::after) carries only a thin border — no fill. Its displacement filter warps the border into an irregular contour. It sits above the card content at z-index 3. The border reads as the visible fiber edge of cut cardstock.

The card itself has overflow: visible so both layers can bleed past its border-radius. Text content sits at z-index 2, unfiltered and crisp.

What did not work

  • CSS mask with SVG turbulence applied a noisy mask to the card boundary. The turbulence ate into the card’s alpha across the board, making the entire card look translucent instead of roughening only the edges.

  • Rough clip-path with hand-drawn wobbly coordinates was the most dramatic option but killed border-radius and merged the card into the background — no contact shadow survived the clip.

  • Noisy border-image overlay used an SVG turbulence texture as a border-image-source. At any opacity low enough not to look garish, the effect was completely invisible.

  • Displacement on the card element itself produced great edge roughness but warped every letter of text inside. The backdrop-shell trick solves this by isolating the filter to a decoration-only layer.

The final recipe

The values that shipped, tuned with the sliders above:

Layer Property Value
Card base background Offset paper texture, luminosity blend, #fcf8f0
Card base contact shadow 0 0.5px 0 0 rgba(30,25,18,0.06), 0 1px 2px -0.5px rgba(30,25,18,0.08)
Card base inset edge inset 0 0 0 0.5px rgba(30,25,18,0.07)
Shell feTurbulence baseFrequency 0.04, octaves 5
Shell feDisplacementMap scale 3
Outline feTurbulence baseFrequency 0.05, octaves 4
Outline feDisplacementMap scale 5
Outline border 1.5 px, rgba(30,25,18, 0.09)

The shell displacement is subtle — scale 3 is barely perceptible. It creates just enough irregularity that the card edge does not read as machine-cut. The outline displacement is heavier (scale 5) because the 1.5 px border is thin enough to absorb more wobble without looking cartoonish.