This is a submission for Frontend Challenge - Comfort Food Edition, CSS Art.
π© Inspiration
I write about WordPress plugins and PHP standards for a living, so when DEV dropped a "Comfort Food" theme for their Frontend Challenge, I didn't need to think twice about what to build. Not ramen, not pancakes β a donut. Specifically, the kind of donut that shows up on your desk right when a deploy breaks and somehow fixes everything.
The twist I gave myself: don't just draw a static donut. Let people build one β pick a glaze, pile on toppings, then serve it β and do almost all of it in CSS, with JavaScript kept firmly in the back seat where the challenge rules ask for it to stay.
That's how Donut Panic was born.
π¬ Demo
Pick a glaze, load it up with sprinkles, drizzle, or powdered sugar, then hit Serve and watch it animate off the plate.
π οΈ Journey
No JavaScript is driving the donut β :has() is
Here's the part I'm most excited to talk about: every visual change in Donut Panic β the glaze swap, the toppings appearing, the donut lifting off the prep station and landing on the plate β is driven by plain checkbox/radio inputs and the :has() selector. Something like .kitchen:has(#serve:checked) .donut lets a parent element react to the checked state of an input buried somewhere inside it, which means the "Serve" button, the topping toggles, and the glaze picker are all just styled <label>s wired to hidden inputs. No click handlers, no state management β the checkbox is the state.
JavaScript only shows up once, and it's not touching the art at all: it smooth-scrolls the stage into view on mobile after you hit Serve, because on a stacked mobile layout the donut can animate off-screen. That's the "sprinkle" of JS the challenge rules allow, used exactly the way it's meant to be β a UX nicety, not a rendering engine.
Building the donut from the inside out
The donut itself is layered rings, not a single flat shape:
- A base dough circle with a radial gradient doing double duty as both color and simulated top-down lighting (highlight near one edge, shadow fading toward the rim).
- A glaze layer sitting on top, clipped to a perfect circle via
overflow: hiddenon a wrapping.ring-clip, so drizzle and sprinkles can never spill past the donut's edge no matter how they're positioned. - A hole, punched through last with its own inset shadow so it always reads as depth rather than a flat cutout, even while the whole donut is mid-animation.
- A shine layer, a diagonal white gradient that sweeps across the glaze on an infinite loop, which is what sells the "glossy glaze" feeling more than any single color choice does.
Making the glaze a real material change
Picking a glaze isn't a color swap β it's a full custom-property swap. Each glaze option (strawberry, chocolate, vanilla) redefines a whole cluster of variables β --glaze, --glaze-dark, --glaze-darker, --glaze-light β the moment its radio button is checked, using :has() again to scope the override to that one selected state. Because the gradient, the shine, and the shadow all reference those same variables, switching glazes updates lighting and depth together instead of just flattening the donut into a different hue.
Toppings as independently toggleable layers
Sprinkles, extra drizzle, and powdered sugar are each their own absolutely-positioned layer, faded in or out purely with opacity and scale transitions tied to their own checkbox. The sprinkles alone are two dozen individually placed spans, each carrying its own --t, --l, --r, and --c custom properties for position, rotation, and color, so they scatter across the donut face believably instead of repeating in an obvious pattern. Because every topping is decoupled from the glaze logic, you can mix and match freely β chocolate glaze with powdered sugar, strawberry with all three toppings at once β without any rule fighting another.
The "Serve" animation
Hitting Serve doesn't just reveal a new state β it moves the donut. The .donut element transitions its top, width, and transform properties with a bouncy cubic-bezier easing, so it visibly travels from the dashed-outline "prep station" down onto the plate, complete with a staggered pop-in for the glaze puddle, stray crumbs, and a handwritten napkin tag that reads "Enjoy Your Donut." The button label itself flips from "Serve the Donut" to "Served! Enjoy π" using the same checked-state trick, and the topping menu visually locks (dimmed, grayscaled, pointer-events: none) so you can't keep editing a donut that's already left the kitchen.
What I'm proud of
Getting an entire multi-step interaction β pick a glaze, pick toppings, serve, watch it land, get locked out of editing β to run on nothing but :has(), custom properties, and transitions is the part I'm happiest with. It's proof that a lot of what we instinctively reach for JavaScript to do is really just state living in the DOM already, if you're willing to let a checkbox be the source of truth.
What's next
If I extend this past the challenge, I'd like to add more glaze and topping combinations, and maybe a "box of donuts" view that remembers everything you've built across a session β a good excuse to push :has() even further before I let JavaScript back in.
πLicense
This project is open source, licensed under MIT. Fork it, remix it, build your own topping bar on top of it β just keep the license notice intact.
Happy Coding </>
Top comments (0)