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.
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.
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.
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.
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:
-
A backdrop shell (
::beforein 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. -
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-radiusand 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.