I Tried Drawing a Rose with CSS
I wanted to see how far CSS could go with trigonometry.
So I tried to draw a seven-petal rose without canvas, SVG, or JavaScript doing the math.
It turns out CSS can do quite a lot. Play with this one first — then I'll show you why it works.
The math
A few discoveries, in the order I hit them.
CSS knows trigonometry now. sin() and cos() aren't just JavaScript functions anymore. You can hand an angle to the stylesheet and get a coordinate back.
A circle is enough to start. A point at angle θ on a circle of radius R sits at:
x = R · cos(θ)
y = R · sin(θ)
Sweep θ all the way around and you've walked a circle. That's the whole foundation.
Change the radius and the circle becomes a flower. A rose curve — a rhodonea — barely changes the recipe. Instead of a fixed R, the radius itself breathes:
r(θ) = cos(k · θ)
Fold that into the circle and you get:
x = cos(kθ) · cos(θ)
y = cos(kθ) · sin(θ)
When k is 7, that's a seven-petal rose.
You don't need to draw a line. CSS can't stroke a continuous parametric path the way SVG can. What it can do is sample the curve: drop a tiny glowing dot at hundreds of angles, and let sin() and cos() place each one.
.dot {
--r: cos(calc(var(--k) * var(--t)));
--x: calc(cos(var(--t)) * var(--r) * var(--scale));
--y: calc(sin(var(--t)) * var(--r) * var(--scale) * -1);
transform: translate(var(--x), var(--y));
}
--t is this dot's angle. --k is shared. The Y term is flipped because screens count downward. Each dot is one sample of the curve; CSS calculates where it belongs.
Playing with it
The weird part. Drag k.
Something strange happens: 1, 3, 5 and 7 produce that many petals, but 2, 4, 6 and 8 suddenly produce twice as many.
That's not a CSS quirk. It's a property of the rose curve itself. Odd k traces every petal in half a turn; even k needs a full turn, and you see 2k petals. Click k = 3 next to k = 8 in the family above — the jump is obvious.
Density is honesty. Too few dots and you see a constellation. Too many and the glow turns into a solid line. The slider isn't a rendering setting so much as a way to watch the sampling idea: the curve was always there; you're just choosing how many points admit it.
Time is a separate job. Once the rose was working, I wanted it to feel like it was being drawn. Rather than animating every dot individually, I used one progress value and let each dot appear as the sweep reached it.
.dot {
opacity: clamp(0, calc((var(--progress) - var(--threshold) + 0.05) / 0.05), 1);
}
CSS handles the geometry. Motion only handles time — animating --progress from 0 to 1 whenever k changes, and skipping the sweep entirely when prefers-reduced-motion is on.
That split is the whole engineering story: CSS does the geometry; React provides the parameters; Motion provides time.
The takeaway
What started as a question about whether CSS could draw a curve turned into a surprisingly nice little playground.
The browser handles the trigonometry, CSS handles the geometry, and Motion handles the movement.
And the fun part is that once the formula is there, changing one number can turn the same equation into an entirely different shape.
That's probably what I like most about this experiment: the code barely changes, but the picture does.
How it works
React only stamps a --t on each dot and writes --k, --scale, and --progress on the parent. Positions never get computed in JavaScript.
const dots = Array.from({ length: count }, (_, i) => {
const turns = k % 2 === 0 ? 1 : 0.5;
return (i / count) * 360 * turns;
});
Odd k closes after half a sweep; even k needs a full one — that's the same petal rule you can feel on the slider.
The little family of shapes under the rose is a static SVG map of the same equation, so you can jump between values without dragging. The large rose is still CSS.
Source for the rose demoThe component behind the playground — CSS variables, the draw sweep, and the presets.(opens in a new tab)