Squircles that never cost you a frame
Figma-accurate corner smoothing for the web. One observer for the whole page, no React re-renders, and borders that follow the curve on a single element.
Tune it, resize it.
Every slider and every drag of the corner goes through the same shared ResizeObserver. The card itself never re-renders.
<Squircle
cornerRadius={48}
cornerSmoothing={1}
borderWidth={1}
border="#d9d9dc"
className="card"
/>Drop-in. Then some.
Same props as @squircle-js/react (cornerRadius, cornerSmoothing, asChild, width, height), plus borders, per-corner radii and a vanilla API.
import { Squircle } from "@fohz67/squircle/react";
export function Card() {
return (
<Squircle cornerRadius={24} cornerSmoothing={1} className="card">
Hello, squircle.
</Squircle>
);
}Renders a div. Until it is measured (SSR, no JavaScript) it falls back to a regular border-radius of the same size.
Built for pages with hundreds of them.
Born on a production store where a single product listing renders around 700 squircles. Here is what that taught us.
One observer, period
A single ResizeObserver serves every squircle on the page. squircle.js creates one per element, plus a window listener each.
Never a frame behind
Sizes never go through React state. The clip-path is written after layout and before paint, so every frame shows the right shape.
Real borders
borderWidth and border draw a ring on the same element. Solid, translucent or gradient, it always follows the curve.
6–11× faster paths
Same Figma geometry, but trigonometry is cached per smoothing value and the path is written as bytes.
Shared cache
Identical cards share one path in an LRU cache. Three hundred same-size product cards compute it once.
SSR and no-JS safe
Server HTML is a normal rounded rectangle, with an inset shadow for borders, until the shape is measured.
Same API. Different engine.
squircle.js made squircles easy in React, and the API here is intentionally compatible. The difference is what happens when there are hundreds of them.
| @fohz67/squircle | @squircle-js/react 1.3 | |
|---|---|---|
| ResizeObservers for N elements | 1 | N |
| window resize listeners | 0 | N |
| React re-renders on resize | None | Every element |
| Shape on first paint | Yes | No, after an effect |
| Stale frames while resizing | 0% | 100%, one frame late |
| Path cache | Shared LRU | Per component |
| Borders | One element, any background | Not supported |
| Per-corner radii | Yes | No |
| Ref forwarding | Yes | No |
| Framework-agnostic core | Yes | React only |
| Runtime dependencies | 0 | 2 |
Benchmark it yourself.
Real components from both libraries, same props, same page. A probe checks right before each paint whether the shape on screen matches the element, then times it.
Stale frames while resizing
1,000 squircles resized for 40 frames, checked right before each paint.
Time to correct shape
Mount 1,000 squircles until every one is clipped. Median of 3.
Path generation
Per path, 20,000 distinct sizes, same Figma algorithm.
Keep this tab visible while it runs: browsers throttle animation frames in background tabs. Timings depend on hardware; the stale-frame count does not.
Main-thread CPU, from Chrome DevTools
A page can't measure its own CPU time, so bench/browser.bench.mjs runs this benchmark in headless Chromium and reads DevTools metrics per phase. Reference run: 1,000 squircles, Apple M-series, median of 3.
| @fohz67/squircle | @squircle-js/react | ||
|---|---|---|---|
| Resize (40 frames), main thread | 249 ms | 523 ms | 2.1× less |
| Resize, JavaScript only | 83 ms | 363 ms | 4.4× less |
| Resize, stale frames | 0% | 100% | always in sync |
| Mount, main thread | 15 ms | 26 ms | 1.7× less |
| Mount, squircled on first paint | yes | no | |
| Path generation, in browser | 760 ns | 4,760 ns | 6.3× faster |
Why not WebAssembly?
We tried. An AssemblyScript build was 2.5× faster than our first JavaScript. Not because of WASM, but because it wrote digits as integers into a byte buffer instead of concatenating float strings. We ported that idea back to JavaScript and the gap closed. Same speed, no async instantiation, no binary to ship.
| Implementation · Node 24, Apple M-series | ns / path | vs figma-squircle |
|---|---|---|
| figma-squircle 1.1 (used by squircle.js) | ~5,500 | 1× |
| First JS version (string concatenation) | ~1,250 | 4.5× |
| AssemblyScript → WebAssembly | ~490 | 11.3× |
| @fohz67/squircle (pure JS, shipped) | ~500 | 11× |
Ship smoother corners.
One import. Same props you already use. Measurably less work on every frame.