hyperellipse

CSS corner-shape polyfill. Consistent rendering across browsers.

Native in Chromium · Spec-accurate fallback for Safari and Firefox

9.3 kB gzip · Framework-agnostic · SSR-friendly

CSS
Settings

Why hyperellipse?

corner-shape adds squircles, scoops, and superellipses — border-radius can't draw those. Only Chromium supports it today; Safari and Firefox drop the property at parse time. hyperellipse makes one stylesheet work everywhere.

One CSS pattern, two paths

Set --corner-shape next to border-radius, call registerHyperellipse() once. Chrome keeps native rendering through a tiny CSS bridge — no observers, no layout thrashing. Everywhere else, a stylesheet scan drives a clip-path fallback.

Matches Chromium

Same border-radius, same squircle in Safari as in Chrome. The fallback tracks CSS Borders 4 superellipse curves instead of hand-rolling clip-path.

Real-world CSS

Gradients, borders, box-shadow, outline with offset — it shapes the box, not an SVG layer on top.

SSR-friendly

Squircles read less round at the same radius. Import hyperellipse/css so SSR doesn't paint a circle before JS kicks in.

Tiny API

One function call. registerHyperellipse() is the whole integration — idempotent, no-op on the server, nothing per-element.

Browser support

Chrome

Native

Edge

Native

Safari

hyperellipse

Firefox

hyperellipse
npm install hyperellipse

Install the Agent Skill so coding agents on skills.sh integrate hyperellipse correctly — SSR snippet, API, and limitations.

npx skills add mikhailmogilnikov/hyperellipse@hyperellipse
  1. 1. Register on the client

    Call registerHyperellipse() once on the client — in App, root layout, or entry file. Safe to call again; no-op during SSR.

    'use client';
        
    import { useEffect } from "react";
    import { registerHyperellipse } from "hyperellipse";
    
    export default function App() {
      useEffect(() => {
        registerHyperellipse();
      }, []);
    
      return <main>...</main>;
    }
  2. 2. Style elements

    Set --corner-shape next to border-radius — the custom property survives in the CSSOM where native corner-shape is dropped.

    Tip: add corner-shape: squircle for native zero-JS rendering.

    .button {
      --corner-shape: squircle;
      border-radius: 45px;
    }
  3. 3. SSR fallback (recommended)

    Squircles look less round than circles at the same radius. Add this global snippet so unsupported browsers set --corner-scale: 0.6 before JS loads.

    @supports not (corner-shape: squircle) {
      :root {
        --corner-scale: 0.6;
      }
    }
    
    /* or @import "hyperellipse/css"; */
    
    .button {
      --corner-shape: squircle;
      border-radius: calc(45px * var(--corner-scale, 1));
    }

API

Supported values

Same shorthand grammar as native corner-shape — 1 to 4 values per element.

--corner-shape: squircle;
--corner-shape: superellipse(4);
--corner-shape: squircle bevel scoop notch;

/* keywords: round, squircle, square, bevel, scoop, notch, superellipse(K) */

Without a stylesheet

Set data-corner-shape on the element when you cannot add a CSS rule.

<div data-corner-shape="squircle" style="border-radius: 32px"></div>

registerHyperellipse()

Idempotent and SSR-safe. Repeated calls return the same controller instance.

import { registerHyperellipse } from "hyperellipse";

const controller = registerHyperellipse({
  selector: ".card", // extra selectors (cross-origin sheets)
  pendingRadiusScale: 0.6, // radius scale after JS loads
  force: false, // force fallback in supporting browsers
});

controller.supported; // native corner-shape?
controller.active; // fallback engine running?
controller.refresh(); // rescan + recompute
controller.destroy(); // tear down

Options

selector

Extra selectors for elements using corner shapes — escape hatch for cross-origin stylesheets.

pendingRadiusScale

Border-radius multiplier in the automatic pending stylesheet after JS loads. Default 0.6. Prefer the CSS --corner-scale snippet for SSR.

force

Force the JS fallback even when the browser supports native corner-shape. Useful for debugging.

Controller

supported

Whether the browser supports native corner-shape.

active

Whether the JS fallback engine is running.

refresh()

Rescan stylesheets and recompute every tracked element.

destroy()

Stop the fallback and remove all applied inline styles.

How it works

registerHyperellipse() checks for native corner-shape . Chromium gets a CSS-only path; Safari and Firefox run the fallback pipeline below.

Chromium

Feature detect

CSS.supports('corner-shape') passes. No fallback engine, no observers.

CSS bridge

Injects @property --corner-shape and corner-shape: var(--corner-shape) at zero specificity. Your CSS drives native rendering.

Fallback

1. Scan

Walks stylesheets for selectors declaring --corner-shape. Also matches [data-corner-shape], inline styles, and any extra selector you pass in.

2. Match

Queries the DOM for matched elements and attaches a shared ResizeObserver to each.

3. Read

Batch-reads computed styles once per animation frame. Temporarily forces --corner-scale: 1 so geometry uses the full radius, not the SSR reduction.

4. Compute

Builds superellipse corner paths from element size, border-radius, and the --corner-shape value.

5. Apply

Solid fills get clip-path: path(...). Borders, shadows, or outlines switch to layer mode — SVG rings and blurred shapes on ::before / ::after.

6. Watch

MutationObserver rescans when stylesheets or attributes change. Pointer enter/leave on the element and its ancestors picks up :hover (including parent selectors); focusin/focusout picks up :focus. transitionrun and transitionend catch the start and end of CSS transitions. A shared IntersectionObserver skips off-screen elements until they enter the viewport. Size-only resizes reuse cached geometry without re-reading computed styles.

Known limitations

Inset shadows

Outer shadows and spread are exact; inset box-shadow is dropped in the fallback.

Non-uniform borders

Dashed, dotted, and per-side borders render as a uniform solid ring.

Layer mode + images

With box-shadow or outline, background images and gradients are not shaped — corners may stick out. Solid fills work fully.

Pseudo-elements & clipping

Layer mode uses ::before/::after and isolation: isolate. Child content is not clipped to the shape.

Dynamic styles

:hover and :focus work in the fallback. The engine re-reads computed styles on pointer enter/leave and focus events on the element and its ancestors — so parent :hover rules (e.g. .wrap:hover .block) are covered too. Call refresh() for imperative updates outside CSS.

CSS transitions

border-radius, box-shadow, and outline transitions are not interpolated frame-by-frame — the shape updates on state change, at transitionrun, and at transitionend. opacity and transform always transition natively. background-color transitions smoothly on solid fills only (no shadow or outline); in layer mode the fill is baked into SVG and jumps. Size (width / height) transitions animate smoothly via ResizeObserver.

Non-inherited property

--corner-shape does not inherit — set it on the element itself, not on an ancestor.

CSS keyframe animations

corner-shape, border-radius, box-shadow, and outline keyframes are not tracked frame-by-frame in the fallback. Size changes (width / height) animate smoothly via ResizeObserver.

Corner shapes

Squircle

.block {
  --corner-shape: squircle;
  border-radius: 28px;
}

Superellipse

.block {
  --corner-shape: superellipse(4);
  border-radius: 28px;
}

Scoop

.block {
  --corner-shape: scoop;
  border-radius: 28px;
}

Notch

.block {
  --corner-shape: notch;
  border-radius: 28px;
}

Per-corner mix

.block {
  --corner-shape: squircle bevel scoop notch;
  border-radius: 28px;
}

Style blocks

Static

Fill

.block {
  --corner-shape: squircle;
  border-radius: 28px;
}

Border

.block {
  --corner-shape: squircle;
  border-radius: 28px;
  border: 2px solid rgb(0 0 0 / 0.2);
}

Box shadow

.block {
  --corner-shape: squircle;
  border-radius: 28px;
  box-shadow: 0 8px 20px rgb(0 0 0 / 0.25);
}

Outline

.block {
  --corner-shape: squircle;
  border-radius: 28px;
  outline: 2px solid rgb(0 0 0 / 0.25);
  outline-offset: 5px;
}

Gradient

.block {
  --corner-shape: squircle;
  border-radius: 28px;
  background: linear-gradient(
    135deg,
    rgb(0 0 0 / 0.15),
    rgb(0 0 0 / 0.05)
  );
}

Pill

.block {
  --corner-shape: squircle;
  border-radius: 50%;
}

Hover

:hover works in Safari and Firefox. Border, shadow, and outline update instantly — not mid-transition.

Hover + border

.block {
  --corner-shape: squircle;
  border-radius: 28px;
  border: 2px solid rgb(0 0 0 / 0.2);
}

.block:hover {
  border-width: 4px;
}

Hover + shadow

.block {
  --corner-shape: squircle;
  border-radius: 28px;
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
}

.block:hover {
  box-shadow: 0 10px 24px rgb(0 0 0 / 0.25);
}

Hover + outline

.block {
  --corner-shape: squircle;
  border-radius: 28px;
  outline: 2px solid rgb(0 0 0 / 0.25);
  outline-offset: 4px;
}

.block:hover {
  outline-width: 4px;
  outline-offset: 6px;
}

Animation

Only size (width / height) is tracked during animation. Other property keyframes are deliberately ignored for performance reasons.

Size + border

@keyframes breathe {
  0%,
  100% {
    width: 3.25rem;
    height: 3.25rem;
  }
  50% {
    width: 5.5rem;
    height: 5.5rem;
  }
}

.block {
  --corner-shape: squircle;
  border-radius: 28px;
  border: 2px solid rgb(0 0 0 / 0.2);
  animation: breathe 2.4s ease-in-out infinite;
}

Size + shadow

@keyframes stretch {
  0%,
  100% {
    width: 5.5rem;
    height: 3.25rem;
  }
  33% {
    width: 3.5rem;
    height: 5rem;
  }
  66% {
    width: 5rem;
    height: 3.5rem;
  }
}

.block {
  --corner-shape: squircle;
  border-radius: 28px;
  box-shadow: 0 8px 20px rgb(0 0 0 / 0.25);
  animation: stretch 3s ease-in-out infinite;
}

Size + outline

@keyframes breathe {
  0%,
  100% {
    width: 3.25rem;
    height: 3.25rem;
  }
  50% {
    width: 5.5rem;
    height: 5.5rem;
  }
}

.block {
  --corner-shape: squircle;
  border-radius: 28px;
  outline: 2px solid rgb(0 0 0 / 0.25);
  outline-offset: 5px;
  animation: breathe 2.4s ease-in-out infinite;
}

Real-world UI

Primary button

squircle · fill · shadow

Secondary button

squircle · border · shadow

Search field

squircle · border

Icon button

squircle · border

Avatar

squircle · fill
Beta

Badge

squircle · fill

List item

squircle · border · shadow

Toast

squircle · border · shadow