v1.0Zero dependencies · ~4 KB gzip

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.

View on GitHub
Anatomy of a corner
r(1 + ξ) · r
Squircle border-radius
1.00
Live components
Order confirmed
Shipped in 24 h · tracking sent
#4821
Search products⌘K
Playground

Tune it, resize it.

Every slider and every drag of the corner goes through the same shared ResizeObserver. The card itself never re-renders.

cornerRadius48px
cornerSmoothing1
border
borderWidth1px
background
no re-render on resize
Resize medrag the bottom-right corner
resizable
Card.tsx
<Squircle
  cornerRadius={48}
  cornerSmoothing={1}
  borderWidth={1}
  border="#d9d9dc"
  className="card"
/>
Usage

Drop-in. Then some.

Same props as @squircle-js/react (cornerRadius, cornerSmoothing, asChild, width, height), plus borders, per-corner radii and a vanilla API.

Card.tsx
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.

Hello, squircle.
Features

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.

Compare

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 elements1N
window resize listeners0N
React re-renders on resizeNoneEvery element
Shape on first paintYesNo, after an effect
Stale frames while resizing0%100%, one frame late
Path cacheShared LRUPer component
BordersOne element, any backgroundNot supported
Per-corner radiiYesNo
Ref forwardingYesNo
Framework-agnostic coreYesReact only
Runtime dependencies02
Performance

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.

Against the published @squircle-js/react 1.3.0, in your browser.

Stale frames while resizing

1,000 squircles resized for 40 frames, checked right before each paint.

@fohz67/squircle-
@squircle-js/react-
-

Time to correct shape

Mount 1,000 squircles until every one is clipped. Median of 3.

@fohz67/squircle-
@squircle-js/react-
-

Path generation

Per path, 20,000 distinct sizes, same Figma algorithm.

@fohz67/squircle-
@squircle-js/react-
-

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 thread249 ms523 ms2.1× less
Resize, JavaScript only83 ms363 ms4.4× less
Resize, stale frames0%100%always in sync
Mount, main thread15 ms26 ms1.7× less
Mount, squircled on first paintyesno
Path generation, in browser760 ns4,760 ns6.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-seriesns / pathvs figma-squircle
figma-squircle 1.1 (used by squircle.js)~5,5001×
First JS version (string concatenation)~1,2504.5×
AssemblyScript → WebAssembly~49011.3×
@fohz67/squircle (pure JS, shipped)~50011×

Ship smoother corners.

One import. Same props you already use. Measurably less work on every frame.