Inspiration
Arroz caldo is the dish that shows up in my kitchen uninvited. Someone's sick, it's raining, a day just needs marking quietly β and there it is, ginger and rice simmering on the stove before I've really decided to make it.
The thing I always loved about it growing up was that the last step was never fixed. My mother never measured the garnish. Chili if you needed heat, calamansi if you needed something sharp to cut through it, a boiled egg if the day called for something that felt like a full meal. The base was always the same. The finishing was always yours.
When I saw this challenge allowed "a sprinkle of JavaScript," I didn't want to just paint a static bowl and call it a day β soup bowls are basically a genre unto themselves in CSS art at this point. I wanted to build the one thing that actually captures what makes this dish comfort food: that you get to decide what goes in it. So the bowl starts nearly bare, and you build it yourself, one click at a time.
Demo
larrymargerum01
/
devto-frontend-challenge-bowl
Dev.to Frontend Challenge - CSS Challenge project inspired by Arroz caldo
Build Your Bowl β CSS Art, Comfort Food Edition
A pure CSS art project that visualizes a bowl of arroz caldo (Filipino chicken rice soup) with interactive toppings. Every visual element β from the broth's simmering glow to the steam rising from the bowl β is crafted with CSS. JavaScript plays only a supporting role, toggling class names to reveal ingredients.
π¨ Features
- Pure CSS Art β All visual effects (gradients, shadows, transforms) are pure CSS
- Container Query Responsive β Scales beautifully from mobile to large displays
- Interactive Toppings β Add garlic, scallion, egg, calamansi, and chili oil
- Hover Previews β See ghosted previews of toppings before clicking
- Stir Animation β Click the bowl or "Stir the pot" button for a swirling animation
- Completion Celebration β A golden ring pulse appears when all toppings are added
- Accessibility β Full keyboard navigation and ARIA attributes
π² The Bowl
This visualization representsβ¦
Click the pot (or hit "Stir the pot") to swirl whatever's currently in the bowl. Hover a topping before clicking it β the bowl will show you a faint preview of what you're about to add. Fill all five and the rim gives you a little something back.
Journey
I went in with a rule for myself: CSS does the drawing, CSS does the motion, JavaScript is only allowed to flip switches. No canvas, no SVG, nothing procedurally drawn by a script β if it moves or changes color, a stylesheet is responsible for it.
That constraint ended up shaping almost every interesting decision in the build:
The rice. Instead of eighty individual grain elements, the whole texture is one zero-size div with an box-shadow list β every grain is just an offset, a size, and a color in a single CSS property. It's a trick I'd seen in other CSS art before but never had a real reason to reach for until I needed to fill a bowl with something that had to look scattered, not gridded.
The interactivity had to be honest. My first instinct was to reach for JavaScript to handle showing/hiding garnishes, but that felt like cheating the "CSS is the star" spirit of the challenge. So each topping's visibility, its pop-in bounce, even the broth quietly warming up as you add things β all of it lives in CSS transitions reacting to a class name. The JavaScript that's left is genuinely boring: it toggles a class and updates one sentence of status text. If you open dev tools, the JS file is short enough to read in ten seconds; the stylesheet is not.
The hover preview was the one that made me happiest. Hovering a topping chip β before you've clicked anything β ghosts a faint preview of that ingredient into the bowl. That's :has() reaching from a button in the control rail all the way into the canvas, with zero JavaScript involved in making it happen. I didn't know I'd end up using :has() for something like this when I started; it came from wanting the bowl to feel responsive to intent, not just clicks.
What I got wrong first. Everything in the bowl β every grain, every scallion ring, every chili sliver β is positioned relative to the bowl's true center, and early on I had one wrapper anchored to a corner instead of the center. Nothing looked broken, exactly, just subtly off in a way that took a while to track down, because the bug was in the coordinate system, not in any one shape. That was a good reminder that in CSS art, your math has to be as disciplined as your gradients.
What I'm proud of. That the idea and the technique are actually the same thing. "Make it your own" isn't just a caption under a picture of soup β it's something you do, and everything that happens when you do it is CSS earning its keep.
What's next. I'd like repeated clicks on a topping to intensify it β more chili, more garlic β and a shareable link that captures whatever bowl you've built, so you can send someone your specific combination instead of just a screenshot.
Built with HTML, CSS (custom properties, container queries, :has(), the box-shadow particle technique), and just enough JavaScript to flip a few switches.

Top comments (0)