Smoothed, SmoothedView & SmoothedCapsule

reactcomponentscraft

This is the companion post to Corner Smoothing on the Web. That article covers the math and the tradeoffs. This one is about the components themselves — what they do, how they look, and how to use them.

All components are zero-dependency, available as shadcn registry items, and work with any React setup.

Smoothed is the latest unified version — it auto-detects capsule vs squircle based on the radius and dimensions, supports per-corner radii, and replaces both SmoothedView and SmoothedCapsule in a single import.

pnpm dlx shadcn@latest add https://kit.roomzer.dev/r/smoothed.json

SmoothedView and SmoothedCapsule are still available as standalone components if you only need one shape:

pnpm dlx shadcn@latest add https://kit.roomzer.dev/r/smoothed-view.json
pnpm dlx shadcn@latest add https://kit.roomzer.dev/r/smoothed-capsule.json

What they look like

SmoothedView applies Figma-style corner smoothing to any element. It works with borders, background colors, backdrop-blur, box-shadows — anything you’d put on a div.

borderless
bordered
inset shadow
"Glass" effect

SmoothedCapsule is the same idea for stadium shapes. The radius is auto-computed from the element’s dimensions, so you just control the smoothing factor and optional border.

Both components play well with other libraries. Here’s a Vaul drawer using SmoothedView for the sheet and SmoothedCapsule for the trigger and handle — with backdrop-blur for that iOS glass panel feel:

Segmented control

SmoothedCapsule with Motion’s layoutId gives you an animated segmented control. The active indicator is a SmoothedCapsule that springs between positions:

all 29 items
SmoothedCapsule · motion layoutId

Filtered card grid

SmoothedView with AnimatePresence and layout animations. Cards enter, exit, and reflow with staggered springs:

Designactive
Motionactive
Layoutarchived
Coloractive
Typearchived
Gridactive
SmoothedView + SmoothedCapsule · AnimatePresence · layout animations

Composability

Both components accept an as prop for rendering as any element or component. Refs are forwarded, so they work with motion.div, button, a, or any custom component.

nesting · different radii
Nested SmoothedViews
Outer radius 20 · Inner radius 12
as={motion.div} · spring animation
Click to expand
Ref forwarding works with motion.div
as="button" · as="a"
as prop · ref forwarding · nesting

Smoothed API ✳︎ latest

The unified component. Pass a large radius (e.g. 9999) and it renders a capsule. Pass a smaller radius and it renders a squircle. Mix per-corner radii and it always renders as a squircle.

<Smoothed
  radius={24}
  smoothing={0.6}
  borderWidth={1}
  borderColor="var(--color-border)"
  as="button"
  className="p-4"
>
  Content
</Smoothed>
PropTypeDefaultDescription
radiusnumber0Base corner radius in px. When ≥ min(width, height) / 2 with uniform corners, auto-switches to capsule mode
smoothingnumber0.6Corner smoothing factor, 0–1. 0 = standard border-radius, 0.6 = iOS default
borderWidthnumber0Border width in px, rendered via ::after pseudo-element
borderColorstring"transparent"Any CSS color value
topLeftRadiusnumberPer-corner radius override (forces squircle mode)
topRightRadiusnumberPer-corner radius override (forces squircle mode)
bottomRightRadiusnumberPer-corner radius override (forces squircle mode)
bottomLeftRadiusnumberPer-corner radius override (forces squircle mode)
asElementType"div"Render as a different element or component

Plus all standard HTML attributes and ref. The blog version also includes MotionProps in its type, so motion.div via as works with full type inference out of the box.


SmoothedView API

Standalone squircle component. For new projects, consider using Smoothed instead — it includes all SmoothedView functionality plus auto capsule detection.

<SmoothedView
  radius={24}
  smoothing={0.6}
  borderWidth={1}
  borderColor="var(--color-border)"
  as="button"
  className="p-4"
>
  Content
</SmoothedView>
PropTypeDefaultDescription
radiusnumberBase corner radius in px (required)
smoothingnumber0.6Corner smoothing factor, 0–1. 0 = standard border-radius, 0.6 = iOS default
borderWidthnumber0Border width in px, rendered via ::after pseudo-element
borderColorstring"transparent"Any CSS color value
topLeftRadiusnumberPer-corner radius override
topRightRadiusnumberPer-corner radius override
bottomRightRadiusnumberPer-corner radius override
bottomLeftRadiusnumberPer-corner radius override
asElementType"div"Render as a different element or component

Plus all standard HTML attributes (className, style, onClick, etc.) and ref.

SmoothedCapsule API

Standalone capsule component. For new projects, consider using Smoothed with radius={9999} instead — same capsule rendering, plus you get squircle mode and per-corner radii for free.

<SmoothedCapsule
  smoothing={0.6}
  borderWidth={1}
  borderColor="var(--color-border)"
  as="button"
  className="px-4 py-2"
>
  Label
</SmoothedCapsule>
PropTypeDefaultDescription
smoothingnumber0.6Corner smoothing factor, 0–1
borderWidthnumber0Border width in px
borderColorstring"transparent"Any CSS color value
asElementType"div"Render as a different element or component

No radius prop — the capsule radius is always min(width, height) / 2, computed automatically via ResizeObserver.

Usage with Motion

Both components accept an as prop, so you can pass motion.div directly. To get full type inference on animation props, add MotionProps to the component’s type:

import type { MotionProps } from "motion/react";

export type SmoothedViewProps = SmoothedViewOwnProps &
  Omit<React.ComponentPropsWithoutRef<"div">, keyof SmoothedViewOwnProps> & {
    ref?: React.Ref<HTMLElement>;
  } & MotionProps;

Then use it like any motion component — animate, layoutId, transition, whileHover all work:

import { motion } from "motion/react";

<SmoothedCapsule
  as={motion.div}
  layoutId="active-tab"
  transition={{ type: "spring", stiffness: 500, damping: 30 }}
  className="bg-white"
/>

The segmented control and card grid demos above both use this pattern.

How it works under the hood

Both components share the same architecture:

Singleton ResizeObserver. Every SmoothedView and SmoothedCapsule on the page shares a single ResizeObserver. The observer maps each element to a callback via a WeakMap, so cleanup is automatic.

Sub-pixel precision. The ResizeObserver reads borderBoxSize (float) instead of clientWidth/clientHeight (integer). This means the clip-path tracks the element’s actual rendered dimensions — including fractional pixels during CSS transitions. Most squircle libraries round to integers, which causes visible edge clipping on elements at sub-pixel widths. The path generator uses toFixed(4), so precision is preserved end-to-end.

LRU path cache. SVG path strings are cached by dimension key. Once a 200×48 capsule at smoothing 0.6 is computed, it’s never recomputed — any other element with the same dimensions reuses the cached path.

Static clip-path. The path is set as clip-path: path('M ...') — a static string that the browser composites like border-radius. No per-frame recalculation, no CSS expression evaluation, no layout thrashing.

Pseudo-element borders. Borders use an ::after element with an evenodd clip path combining the outer and inset inner paths. This avoids the clip-path-clips-borders problem entirely.

Cap Bézier (SmoothedCapsule). Standard squircle constructions produce a flat spot at the capsule apex — the two mirrored short-side Bézier segments meet with zero curvature. SmoothedCapsule replaces them with a single “cap Bézier” that is tangent-continuous with both arcs and passes through the bounding box edge with real curvature. The result is a capsule that looks uniformly smooth from end to end.

Auto shape detection (Smoothed). The unified component checks whether radius >= min(width, height) / 2 with all four corners uniform and the element non-square. If so it switches to the capsule path builder, otherwise it uses the squircle path builder. Setting any per-corner radius override forces squircle mode. This is a zero-cost branch — just a comparison before path generation.

Style injection. A single <style> tag is injected once for the base classes (.smoothed-base, .smoothed-border for Smoothed, .squircle-base, .squircle-border for SmoothedView, .capsule-base, .capsule-border for SmoothedCapsule). No per-instance style tags.

Performance

The previous post showed that CSS shape() with trig functions is catastrophically slower than a static clip-path: path() during dimension-changing animations. But what about corner-shape: superellipse() — the native CSS proposal?

I benchmarked all three approaches across scenarios ranging from gentle (15 elements, no CPU load) to abusive (50 elements morphing under heavy load). CSS superellipse, SmoothedView (squircle path), and SmoothedCapsule (capsule path) are compared head-to-head:

TestCSS superellipseSmoothedViewSmoothedCapsule
Baseline 15el120 fps / 0% drop120 fps / 0% drop120 fps / 0% drop
Baseline 50el113.5 fps / 0% drop90.3 fps / 0.2% drop119.8 fps / 0% drop
Real-world 30el91.2 fps / 0% drop81.5 fps / 0% drop85.4 fps / 0% drop
Real-world 30el fast90.5 fps / 0% drop90.8 fps / 0% drop93.2 fps / 0% drop
Layout 15el120.1 fps / 0% drop120.7 fps / 0% drop120 fps / 0% drop
Layout 30el112.7 fps / 0% drop112.8 fps / 0% drop112.4 fps / 0% drop
Shuffle 30el103.6 fps / 0% drop101.6 fps / 0% drop100.4 fps / 0% drop
Morph 15el119.4 fps / 0% drop119.8 fps / 0% drop119.8 fps / 0% drop
Morph 30el82.5 fps / 4.6% drop81.9 fps / 3.4% drop80.9 fps / 4% drop
Stress 30el heavy43.7 fps / 0.9% drop42.3 fps / 1.9% drop42.6 fps / 1.9% drop
Stress 50el morph49.3 fps / 10.2% drop44.9 fps / 12.1% drop48.8 fps / 10.3% drop

The short version: they’re essentially the same. CSS superellipse has a slight edge under extreme stress (50 elements morphing — a scenario you’d never ship), but in any realistic workload the difference is within noise. The path-based approach runs at 97–100% of native CSS performance while producing a more accurate curve and giving you full control over the smoothing factor, per-corner radii, and capsule shapes that superellipse() can’t express at all.

Gotchas

overflow: visible — both components set overflow: visible on the base element so the ::after border pseudo-element isn’t clipped. If you need overflow hidden, wrap the content in an inner div.

clip-path clips everything — including box-shadow. Inset shadows work fine (they’re inside the clip). Outset shadows need to go on a wrapper element. backdrop-filter works because it applies before clipping.

Per-corner radii — SmoothedView and Smoothed support them, SmoothedCapsule doesn’t (by design — capsules have uniform corners). In Smoothed, setting any per-corner override forces squircle mode even if the base radius is large enough for a capsule.

SSR — the components render their children server-side as a plain element. The clip-path is applied on mount via ref, so there’s a single paint where the element has standard border-radius before the smoothed path kicks in. In practice this is invisible.