Corner Smoothing on the Web

cssperformancecraft

I’ve been building Whaaly, a crypto portfolio tracker, and at some point I decided every rounded rectangle in the app deserved that clean iOS vibe. That smooth, continuous curve that makes a boring shape feel inexplicably pleasing to the eye.

What started as a cosmetic preference turned into weeks of (almost useless) geometry, a detour through a 1984 CAD paper, and a performance lesson I didn’t see coming.

Not a superellipse

Apple introduced corner smoothing in iOS 7 and designers have been chasing it ever since. Most people assume the shape is a superellipse. It’s not.1

CSS is getting a native superellipse() function for corner-shape. That’s exciting, but even when it ships widely, it won’t quite get you there. A superellipse is a close approximation of what Apple and Figma actually render, but it’s a different curve — and the differences show up in exactly the places that matter most. Borders thicken at the apex of each corner because the inner and outer superellipses aren’t truly parallel. The curvature profile doesn’t match the iOS feel. You get a smooth corner, but not that smooth corner.

Side-by-side comparison of corner-shape: superellipse(1.5) versus standard border-radius, showing reduced rounding and border thickening at the apex on the superellipse

Notice two things: the superellipse produces less rounding for the same corner-radius value, and the border visibly thickens at the diagonal apex of each corner. In fact it gets even worse at superellipse(2) (default squircle value for corner-shape). Both are artifacts of the superellipse function that a Bézier-arc construction avoids.

What Apple and Figma use is a construction based on cubic Bézier curves spliced to circular arcs. Daniel Furse’s 2018 Figma blog post2 traces the full journey through the math — from superellipses to clothoid spirals to the final practical solution. It’s a great read. The figma-squircle library3 by phamfoo implements it as an SVG path generator.

The initial SVG approach

My first implementation called figma-squircle directly. Generate an SVG path string, set it as clip-path: path('...'), done. For borders, generate a second smaller path, combine both with evenodd fill rule, apply to a ::after pseudo-element.

It worked, but each element created its own <style> tag, its own ResizeObserver, and on every resize ran: two path generations, a regex parse, string concatenation, and a CSSOM mutation. At 100 animating elements: 25 FPS. At 500: 6 FPS, and 500 <style> tags in <head>.

The CSS rewrite

CSS shape()4 shipped across browsers in February 2026, bringing calc() with trig functions (sin, cos, tan) to clip paths. The entire Figma squircle formula translates directly into CSS custom properties — the browser resolves everything during its native style pass.

<CssSmoothedView
  radius={32}
  smoothing={0.6}
  borderWidth={2}
  borderColor="#1e40af"
  className="bg-blue-500 p-6 hover:scale-105 transition-transform"
>
  Content
</CssSmoothedView>

The element is just a div with a clip-path. Hover states, transitions, Tailwind classes — everything works normally. The only JS is a singleton ResizeObserver shared across all instances that sets one CSS variable: --view-budget (half the element’s smaller dimension).

Early (bad) benchmarks: 102 FPS at 100 elements versus 25 for the SVG approach. Zero injected style tags. We shipped it.

The border rabbit hole

Getting the shape right was a weekend. Getting the borders right took weeks.

Every squircle library I found computes the inner border path by simply shrinking the radius and recomputing a smaller squircle. The border is the gap between the two shapes.

This is wrong. A smaller squircle isn’t a parallel offset of the larger one — it’s a proportionally scaled copy. For Bézier curves, the true parallel curve is a degree-10 algebraic monster. The practical result: borders visibly thicken in the corner transitions.

I found a 1984 paper by Tiller and Hanson5Offsets of Two-Dimensional Profiles — that describes a control-polygon offsetting technique for computing approximate parallel Bézier curves. For our specific squircle geometry, the entire correction collapses to a single expression: E = borderWidth × tan(β/2) where β = 45° × smoothing. The half-angle identity does all the heavy lifting.

It solved the uniform thickness problem. But it introduced a curvature kink at the Bézier-arc junction — the inner path lost its smoothness.

I discovered this by accident, nesting two CssSmoothedViews. An inner one at radius r - borderWidth with the exact same formula looked perfectly smooth — because it was a natural squircle, not a corrected one. That settled it: I use a natural inner squircle. The border is slightly thicker at the diagonal (~s × bw), but the curvature is continuous everywhere. At typical border widths, the thickness variation is sub-pixel.

Plot twist

Then I went back and optimized the SVG approach. Singleton observer, LRU path cache, geometry/style prop split so color changes don’t trigger path recomputation. Four structural changes, no algorithmic changes.

And ran a PROPER benchmark suite this time.

Results

I tested across 11 scenarios — varying element counts (15–50), CPU load (idle to heavy), animation types (transitions, layout reflows, enter/exit, continuous spring transforms), and re-render intervals. Each test runs 5 seconds per implementation, measuring FPS, P95 frame time, and dropped frames (>33ms).

SmoothedView won every single test.

TestCssSmoothedViewSmoothedViewRatio
Baseline 15el56.8 fps / 14% drop115.4 fps / 0% drop
Baseline 50el14.8 fps / 26% drop105.8 fps / 2% drop
Real-world 30el25.8 fps / 20% drop79.0 fps / 1% drop
Layout 30el55.8 fps / 10% drop92.7 fps / 5% drop1.7×
Morph 30el19.5 fps / 53% drop58.7 fps / 17% drop
Stress 50el7.2 fps / 97% drop27.8 fps / 35% drop3.9×

The gap widens with element count, animation pressure, and CPU load. Under stress, CssSmoothedView drops 97% of frames.

Why those results

clip-path: path('M 25.3 64.7 ...') is a static string. The browser composites it like a border-radius — basically free.

clip-path: shape(from 0 var(--p), curve to var(--d) calc(...) ...) is a live expression. During any dimension-changing animation, 100% changes every frame, which invalidates every calc(), which re-evaluates every sin(), cos(), tan(), sqrt(). Per element. Per frame. On the main thread.

The optimized SVG approach computes the path once, caches it, and lets the compositor handle a static string. CSS trig functions aren’t free when they run every frame.6

Where it landed

Both implementations live in the codebase. CssSmoothedView for static or lightly-animated contexts. SmoothedView for anything that animates at scale. Same border approach in both — natural inner squircle, curvature continuity over geometric perfection.

<SmoothedView
  cornerRadius={32}
  cornerSmoothing={0.6}
  borderWidth={2}
  borderColor="#1e40af"
  className="bg-blue-500 p-6 hover:scale-105 transition-transform"
>
  Content
</SmoothedView>

Try it yourself — drag the smoothing slider from 0 to 1 and watch the difference against plain border-radius:

SmoothedView
border-radius
40px
iOS
0.6
2px
cornerSmoothing: 0.6 · cornerRadius: 40px

Why I spent so much time on this shi

Here’s the thing. Most users will never notice the difference between a squircle and a rounded rectangle. They won’t see the border thickening at the diagonal. They won’t feel the curvature kink that the Tiller-Hanson correction introduces. They definitely won’t care whether the shape is a superellipse or a Bézier-arc construction.

And that’s fine. That’s the point, actually.

The best details are the ones people don’t notice — because when they’re wrong, people feel it without knowing why. A shape that looks slightly off. An animation that stutters for a frame. A border that thickens in the corners. None of these register consciously, but they accumulate into a feeling: this doesn’t feel quite right.

We’re in an era where shipping is cheap. AI writes the boilerplate, frameworks handle the plumbing, and you can go from idea to deployed product in a weekend. That’s genuinely great. But it also means the floor has risen — the baseline product is better than it’s ever been, and what separates good from great is shrinking into these invisible details.

I think that’s what makes design engineering its own discipline. Not the code, not the math, not the pixels — but the willingness to care about something most people will never see, because the cumulative effect of a thousand invisible choices is what makes a product feel crafted instead of merely built.

A squircle is a small thing. But small things are the whole game.

Footnotes

  1. Early reverse-engineering by Marc Edwards7 in 2013 suggested n ≈ 5. Follow-up work by Mike Swanson8 (using genetic algorithms!) and Manfred Schwind9 (who looked directly at the iOS code) showed it was a Bézier construction. The superellipse is close but systematically wrong.

  2. Daniel Furse — Desperately Seeking Squircles, Figma, 2018. Traces the full journey from superellipses to clothoid spirals to the final Bézier-arc construction used by Figma.

  3. phamfoo — figma-squircle. Open-source implementation of Figma’s squircle as an SVG path generator.

  4. CSS Values and Units Module Level 4 — shape(), W3C. The CSS function that enables trig-based squircle clip paths natively.

  5. Wayne Tiller & Eric Hanson — Offsets of Two-Dimensional Profiles, IEEE Computer Graphics and Applications, vol. 4, no. 9, pp. 36–46, 1984. Control-polygon offsetting technique for computing approximate parallel Bézier curves.

  6. shape() is still great for static shapes or shapes that only change when custom properties change. The problem is specifically: many elements × dimension-changing animations × heavy trig expressions. That combination makes the CSS engine the bottleneck.

  7. Marc Edwards — The hunt for the squircle, Apply Pixels, 2017. Reverse-engineered the iOS icon shape and suggested n ≈ 5.

  8. Mike Swanson — Unleashing genetic algorithms on the iOS 7 icon, 2013. Used genetic algorithms to fit a superellipse exponent to iOS 7 icons.

  9. Manfred Schwind confirmed it was a Bézier construction by examining iOS code directly. His findings are documented in the Figma blog post below.