This is a submission for Frontend Challenge: Comfort Food Edition, CSS Art: Comfort Food.
Inspiration
For me, comfort food isn't a dish — it's a ritual. A tulip-shaped glass of çay (Turkish tea), brewed dark "tavşan kanı" (rabbit's blood red, as we say), served on a red-and-white saucer with two sugar cubes, and a simit — the sesame-crusted bread ring sold on every street corner in Türkiye. It's breakfast, it's a work break, it's what you're offered the moment you walk into someone's home.
I wanted to see how close I could get to a photograph of that moment using nothing but HTML and CSS. No images, no SVG, no data URIs, no icon fonts — every pixel is gradients, shadows, masks, and clip-paths.
Demo
https://codepen.io/melik-baryank/pen/OPWYegq
When the scene loads, a porcelain demlik (teapot) floats in, tilts, and pours the tea in front of you — the glass fills from empty, the level speeding up through the narrow waist and slowing in the wide sections, exactly like a real pour. Only when the teapot leaves does the steam start to rise.
Then the scene becomes a playground — little call-out labels point straight at each prop, and every prop keeps glowing until you've tried it:
- 🧊 Click a sugar cube — it tumbles through the air in a real parabolic arc, plops through the glass mouth, rings the surface, and fizzes as it dissolves.
- 🥄 Click the spoon — it lifts off the saucer, dips into the glass, stirs (watch the surface swirl), and settles back where it was.
- 🥯 Click the simit — it shakes and sheds a few sesame seeds onto the table.
- ⟳ "brew again" re-brews everything from the empty glass, and moving your mouse tilts the whole scene in subtle 3D.
Every pixel and every movement is CSS. JavaScript never animates anything — it's a small orchestrator that only adds and removes classes; with JS off or prefers-reduced-motion on, you simply get the finished, steaming still life.
Journey
The whole scene is one HTML file: ~1050 lines of CSS and a ~15-line script. Everything is sized in em, so a single font-size: clamp(...) on the scene scales the entire artwork responsively.
The parts I'm most proud of:
The pour choreography. The entire opening sequence — teapot in, pour, fill, teapot out, steam up — runs on a single 9-second CSS animation duration shared by every element. Each element encodes its own action window as keyframe percentages, so everything stays in perfect sync with zero JavaScript timing code. The tea level's keyframes are sampled from the same Bézier polygon as the glass silhouette: the surface ellipse animates its top/left/width through the glass profile, and the fill accelerates through the narrow waist and slows in the wide bulb — the constant-volume illusion. The stream inside the glass shrinks with a synchronized scaleY, so it always ends exactly at the rising surface. And because the static CSS is the final frame, the .play class only describes the journey: no JS (or reduced motion) means you just see the finished glass.
The tulip glass (ince belli bardak). The silhouette is a pair of polygons sampled from Bézier curves — one for the outer wall, one for the inner. The glass wall itself is cut with clip-path: polygon(evenodd, ...): the outer path minus the inner path leaves just the glass "flesh". A third even-odd pair creates the dark band where the tea touches the wall. Inside the glass, backdrop-filter: blur() saturate() refracts whatever is behind it, like real glass does.
The simit twist. A simit isn't just a torus — the dough is rolled and twisted, so the ridges spiral. I built it from 7 repeating-conic-gradient strips, each masked to a thin radial band, with the conic phase (from angle) advancing as the radius grows. Ridges that shift angle with radius read as a spiral twist. On top of that: 325 sesame seeds and ~120 crust-bubble highlights/shadows, all layered box-shadows in three tone groups. The whole ring lies flat on the table: a scaleY(.54) perspective squash, plus a second copy of the ring shifted down and cut to its bottom half (the ring via mask, the half via clip-path — engine-proof, no mask-composite required) for the visible front wall, and a dark crescent inside the hole for the far inner wall.
Making the interactions discoverable. Interactive CSS art has an obvious problem: nothing tells you it's interactive. So each prop announces itself — a hairline leader line runs from a small label ("drop sugar", "stir", "shake") to the thing it names, the props take turns pulsing with a warm drop-shadow glow, and hover or keyboard focus answers with the same glow. Each call-out and its glow retire the moment that prop is used (one used-* class), so the scene quietly returns to being a photograph once you've explored it.
The interactions. Sugar, spoon and simit are real <button>s (keyboard-accessible and labelled), but the physics is pure CSS. The sugar throw is a genuine parabola from two composed animations: linear X on the button, gravity-eased Y on the cube inside it — no JavaScript math anywhere. The stir is one long keyframe track (lift → two circular laps → settle) with a conic-gradient vortex spinning inside the surface ellipse, clipped by overflow:hidden. JS is only an orchestrator: it toggles classes and resets state on replay.
Depth of field. The café background is blurred with a masked backdrop-filter strip near the horizon, so the subject stays sharp while the background falls out of focus — the CSS version of a wide aperture.
The saucer. Circles flattened with transform: scaleY(.32) to fake perspective. The scalloped red rim is 54 box-shadow circles placed on a ring; the tick patterns are repeating-conic-gradients masked to thin annuli; the warm glow the tea casts on the porcelain is a pair of mix-blend-mode: screen radial gradients.
The wood table. Six stacked repeating-linear-gradients at slightly different angles for grain, a hand-tuned gradient for plank seams that get denser toward the horizon (perspective!), multiply-blended radial gradients for knots, and a sub-pixel dot grid for pores.
The finishing pass. Like a photo, the scene gets a color grade (vignette + warm lift) and a very faint film grain overlay — two full-scene layers that pull everything together.
Accessibility. The scene is a role="group" with a descriptive aria-label, and every interactive prop inside it is a real, labelled <button> you can reach with the keyboard. The replay control lives outside the scene, and under prefers-reduced-motion every animation — pour, sugar, stir, ambient light — is replaced by the finished still frame.
What I learned: box-shadow is a particle system, repeating-conic-gradient is a lathe, and the even-odd fill rule is the closest thing CSS has to boolean geometry. And that I can't look at this piece without craving a simit.
Afiyet olsun! 🫖
Top comments (0)