Squircle - round the edges differently than everyone else
May 15, 2025 (Updated: June 19, 2025)Squircle is a great alternative to regular corner rounding. In this article, I will show you how to get this effect and use it in your projects.
If you're just here for the ready to use solution and aren't interested in the details, you can download the complete project, go to the GitHub project, or install the npm package:
npm install batstack-squircle
npm install batstack-squircle
yarn add batstack-squircle
yarn add batstack-squircle
Since you've found your way here, you've probably already heard about this interesting shape called squircle.
It was popularized mainly by Apple, who uses it in their interfaces. The reason is simple - more natural, soft rounded corners look aesthetically pleasing and subtly stand out from those simple, ruler-straight ones.
In interfaces, using it makes the most sense for images. CSS unfortunately doesn't offer a direct way to create such shapes. However, you can achieve this effect quite easily, and in this article, I'll show you how to do it.
This is the simplest case. When an element has defined dimensions, you can generate an SVG path and use it as a clip-path
to cut the image into a squircle shape.
<img class="squircle" src="http://picsum.photos/200/200" width="200px" height="200px" />
<img class="squircle" src="http://picsum.photos/200/200" width="200px" height="200px" />
.squircle { clip-path: path("M 0 100 C 0 9.62 9.62 0 100 0 C 190.38 0 200 9.62 200 100 C 200 190.38 190.38 200 100 200 C 9.62 200 0 190.38 0 100 Z"); }
.squircle { clip-path: path("M 0 100 C 0 9.62 9.62 0 100 0 C 190.38 0 200 9.62 200 100 C 200 190.38 190.38 200 100 200 C 9.62 200 0 190.38 0 100 Z"); }

If you're wondering where to get this path, my online tool can help you. You can find it here: SVG squircle generator.
This is where things get a bit more complicated. We can't use a pre-generated path - we need to generate it on the fly as the element changes dimensions.
The path is defined by point coordinates (Bézier curves, more on that later). We can't use percentage values in it. We'll therefore be forced to observe element dimension changes to get pixel values.
In JavaScript, we have ResizeObserver at our disposal, which allows us to observe such dimension changes.
Width: 0px
Height: 0px
import { type RefObject, useEffect, useState } from 'react'; interface Dimensions { width: number; height: number; } export function useResizeObserver(ref: RefObject<Element | null>): Dimensions { const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 }); useEffect(() => { if (!ref.current) return; const resizeObserver = new ResizeObserver(entries => { for (const entry of entries) { const { width, height } = entry.contentRect; setDimensions({ width, height }); } }); resizeObserver.observe(ref.current); return () => { resizeObserver.disconnect(); }; }, [ref]); return dimensions; }
import { type RefObject, useEffect, useState } from 'react'; interface Dimensions { width: number; height: number; } export function useResizeObserver(ref: RefObject<Element | null>): Dimensions { const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 }); useEffect(() => { if (!ref.current) return; const resizeObserver = new ResizeObserver(entries => { for (const entry of entries) { const { width, height } = entry.contentRect; setDimensions({ width, height }); } }); resizeObserver.observe(ref.current); return () => { resizeObserver.disconnect(); }; }, [ref]); return dimensions; }
import { useRef } from 'react'; import { ResizableContainer } from './ResizableContainer'; import { useResizeObserver } from './useResizeObserver'; export const App = () => { const containerRef = useRef<HTMLDivElement>(null); const dimensions = useResizeObserver(containerRef); return ( <> <p>Width: {dimensions.width}px</p> <p>Height: {dimensions.height}px</p> <ResizableContainer style={{ width: '300px', height: '300px' }}> <div ref={containerRef} style={{ width: '100%', height: '100%' }} /> </ResizableContainer> </> ); };
import { useRef } from 'react'; import { ResizableContainer } from './ResizableContainer'; import { useResizeObserver } from './useResizeObserver'; export const App = () => { const containerRef = useRef<HTMLDivElement>(null); const dimensions = useResizeObserver(containerRef); return ( <> <p>Width: {dimensions.width}px</p> <p>Height: {dimensions.height}px</p> <ResizableContainer style={{ width: '300px', height: '300px' }}> <div ref={containerRef} style={{ width: '100%', height: '100%' }} /> </ResizableContainer> </> ); };
We'll generate the squircle using Bézier curves, which are easily defined in SVG. This is also the most popular way of drawing curves in vector graphics.
A Bézier curve consists of a starting point, an ending point, and control points that define its shape, and can be of different degrees. The degree determines the number of control points. In this project, we'll use second-degree curves, meaning we'll define two control points.
In the example below, the starting and ending points are defined as { x: 0, y: 0 }
and { x: 0, y: 200 }
, and you can freely move the control points to see how the curve's shape changes.
Our shape consists of four such curves, stretched between the centers of the element's edges. For simplicity, let's place the control points in the corners of the element.
Result:
[ [ // curve 1 { x: 0, y: 150} // starting point { x: 0, y: 0} // control point 1 { x: 0, y: 0} // control point 2 { x: 125, y: 0} // ending point ], // curve 2 [{ x: 125, y: 0}, { x: 250, y: 0}, { x: 250, y: 0}, { x: 250, y: 150}], // curve 3 [{ x: 250, y: 150}, { x: 250, y: 300}, { x: 250, y: 300}, { x: 125, y: 300}], // curve 4 [{ x: 125, y: 300}, { x: 0, y: 300}, { x: 0, y: 300}, { x: 0, y: 150}], ]
[ [ // curve 1 { x: 0, y: 150} // starting point { x: 0, y: 0} // control point 1 { x: 0, y: 0} // control point 2 { x: 125, y: 0} // ending point ], // curve 2 [{ x: 125, y: 0}, { x: 250, y: 0}, { x: 250, y: 0}, { x: 250, y: 150}], // curve 3 [{ x: 250, y: 150}, { x: 250, y: 300}, { x: 250, y: 300}, { x: 125, y: 300}], // curve 4 [{ x: 125, y: 300}, { x: 0, y: 300}, { x: 0, y: 300}, { x: 0, y: 150}], ]
M 0 150 C 0 0 0 0 125 0 C 250 0 250 0 250 150 C 250 300 250 300 125 300 C 0 300 0 300 0 150 Z
M 0 150 C 0 0 0 0 125 0 C 250 0 250 0 250 150 C 250 300 250 300 125 300 C 0 300 0 300 0 150 Z
If you're observant, you might notice that for each curve, we generate 4 points, but in the output string, we only use 3. This is due to how paths are drawn in SVG. When drawing the next curve, we don't start "from scratch," but from where we finished drawing the previous one. Hence, the first point of a new curve is the ending point of the previous one.
// cp1X, cp1Y - control point 1 // cp2X, cp2Y - control point 2 // endX, endY - end point C cp1X cp1Y cp2X cp2Y endX endY
// cp1X, cp1Y - control point 1 // cp2X, cp2Y - control point 2 // endX, endY - end point C cp1X cp1Y cp2X cp2Y endX endY
M 0 150 // move cursor to position [0, 150] C 0 0 0 0 125 0 // draw Bézier curve from [0, 150] to [125, 0] C 250 0 250 0 250 150 // draw Bézier curve from [125, 0] to [250, 150] C 250 300 250 300 125 300 // draw Bézier curve from [250, 150] to [125, 300] C 0 300 0 300 0 150 // draw Bézier curve from [125, 300] to [0, 150] Z // end drawing
M 0 150 // move cursor to position [0, 150] C 0 0 0 0 125 0 // draw Bézier curve from [0, 150] to [125, 0] C 250 0 250 0 250 150 // draw Bézier curve from [125, 0] to [250, 150] C 250 300 250 300 125 300 // draw Bézier curve from [250, 150] to [125, 300] C 0 300 0 300 0 150 // draw Bézier curve from [125, 300] to [0, 150] Z // end drawing
Now, take a look at the implementation and usage example.

export function generateSimpleSquirclePath(width: number, height: number) { const curves = [ [ { x: 0, y: height / 2 }, { x: 0, y: 0 }, { x: 0, y: 0 }, { x: width / 2, y: 0 }, ], [ { x: width / 2, y: 0 }, { x: width, y: 0 }, { x: width, y: 0 }, { x: width, y: height / 2 }, ], [ { x: width, y: height / 2 }, { x: width, y: height }, { x: width, y: height }, { x: width / 2, y: height }, ], [ { x: width / 2, y: height }, { x: 0, y: height }, { x: 0, y: height }, { x: 0, y: height / 2 }, ], ]; return { pathString: _getSVGPathString(curves), curves, }; }; const _getSVGPathString = (curves: { x: number; y: number }[][]) => [ `M ${curves[0][0].x} ${curves[0][0].y}`, ...curves.map( (points) => `C ${points[1].x} ${points[1].y} ${points[2].x} ${points[2].y} ${points[3].x} ${points[3].y}` ), 'Z', ].join(' ');
export function generateSimpleSquirclePath(width: number, height: number) { const curves = [ [ { x: 0, y: height / 2 }, { x: 0, y: 0 }, { x: 0, y: 0 }, { x: width / 2, y: 0 }, ], [ { x: width / 2, y: 0 }, { x: width, y: 0 }, { x: width, y: 0 }, { x: width, y: height / 2 }, ], [ { x: width, y: height / 2 }, { x: width, y: height }, { x: width, y: height }, { x: width / 2, y: height }, ], [ { x: width / 2, y: height }, { x: 0, y: height }, { x: 0, y: height }, { x: 0, y: height / 2 }, ], ]; return { pathString: _getSVGPathString(curves), curves, }; }; const _getSVGPathString = (curves: { x: number; y: number }[][]) => [ `M ${curves[0][0].x} ${curves[0][0].y}`, ...curves.map( (points) => `C ${points[1].x} ${points[1].y} ${points[2].x} ${points[2].y} ${points[3].x} ${points[3].y}` ), 'Z', ].join(' ');
import { ResizableContainer } from './ResizableContainer'; import { SimpleSquircleContainer } from './SimpleSquircleContainer'; export const App = () => { return ( <> <ResizableContainer style={{ width: '300px', height: '200px' }}> <SimpleSquircleContainer style={{ width: '100%', height: '100%' }}> <img src='https://picsum.photos/300/200' alt='img' draggable={false} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> </SimpleSquircleContainer> </ResizableContainer> </> ); };
import { ResizableContainer } from './ResizableContainer'; import { SimpleSquircleContainer } from './SimpleSquircleContainer'; export const App = () => { return ( <> <ResizableContainer style={{ width: '300px', height: '200px' }}> <SimpleSquircleContainer style={{ width: '100%', height: '100%' }}> <img src='https://picsum.photos/300/200' alt='img' draggable={false} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> </SimpleSquircleContainer> </ResizableContainer> </> ); };
This solution works perfectly fine, but it has one significant drawback - it doesn't allow setting the roundness level. The fixed position of control points also means that the rounding is too large when the element has a high aspect ratio, i.e., when it's very short but wide.
However, if these limitations aren't important to you, you can use this solution. It will work well in most cases and is more efficient than the full implementation.
The position of control points should be dynamic to provide a similar level of rounding regardless of the element's dimensions. To control the rounding, we'll introduce an additional roundness
parameter to the function. It will control the offset of control points.
The curves will create a more circular shape when the control points are shifted closer to the starting and ending points on the tangent axis. As they move away, the effect will be sharper.
The fact that the curve extends beyond the element is not a mistake. Since we're using it to cut the shape, the effect in this case will simply be an uncut element, meaning no rounding.
roundness
by design takes values from 0
to 1
.
0
means a completely sharp shape
1
means a circular shape
Let's start with the simpler case - obtaining a shape close to a circle. Bézier curves cannot create a perfect circle. However, you can set the control points to achieve a similar effect. Just apply a constant:
This is the coefficient that determines how far from the starting and ending points on the tangent axis the control points must be for a 1x1 element to represent a quarter circle.
This means that the offset for any dimensions is:
const getShiftForRoundCurves = (width: number, height: number) => { const scale = 0.5522847498307933; return [(width / 2) * scale, (height / 2) * scale] as const; };
const getShiftForRoundCurves = (width: number, height: number) => { const scale = 0.5522847498307933; return [(width / 2) * scale, (height / 2) * scale] as const; };
If we want to achieve no rounding, it might seem simple - just set the offset value large enough so that the curve is outside the element and doesn't cut it. Unfortunately, it's not that trivial.
While we want the curve to extend beyond the element, it should also be as close as possible to the edge. Ideally, it should touch the corners. This way, even low roundness
values, like 0.1
, will give a visible effect by cutting a small portion of the corners.
The function that accomplishes this task and works for an element of any dimensions is a bit more complicated. It adjusts the offset strength depending on the element's proportions - the more the shape deviates from a square, the further the control points are pushed out - in such a way that the curve always "hugs" the corners.
const getShiftForHuggingCurves = (width: number, height: number) => { if (width === 0 && height === 0) return [0, 0] as const; if (width === 0) return [0, height / 2] as const; if (height === 0) return [width / 2, 0] as const; const ratio = width / height; const effectiveRatio = Math.max(ratio, 1 / ratio); const pullStrength = 0.5 / Math.pow(effectiveRatio, 0.2); const long = 1 + pullStrength / (3 * (1 - pullStrength)); const short = (1 + 2 * pullStrength) / (3 * pullStrength); const scaleX = width >= height ? long : short; const scaleY = width >= height ? short : long; return [(width / 2) * scaleX, (height / 2) * scaleY] as const; };
const getShiftForHuggingCurves = (width: number, height: number) => { if (width === 0 && height === 0) return [0, 0] as const; if (width === 0) return [0, height / 2] as const; if (height === 0) return [width / 2, 0] as const; const ratio = width / height; const effectiveRatio = Math.max(ratio, 1 / ratio); const pullStrength = 0.5 / Math.pow(effectiveRatio, 0.2); const long = 1 + pullStrength / (3 * (1 - pullStrength)); const short = (1 + 2 * pullStrength) / (3 * pullStrength); const scaleX = width >= height ? long : short; const scaleY = width >= height ? short : long; return [(width / 2) * scaleX, (height / 2) * scaleY] as const; };
As a result of using the above functions, we get an offset in pixels. However, as the roundness
argument, we provide a value in the range from 0 to 1. We therefore need to transform the roundness value into an appropriate scale, which is the offset in pixels. For this purpose, we'll use the mapValue
function, which transforms a value from one range to another.
const mapValue = ( value: number, fromMin: number, fromMax: number, toMin: number, toMax: number ) => { const fromRange = fromMax - fromMin; const toRange = toMax - toMin; const normalizedValue = (value - fromMin) / fromRange; return toMin + normalizedValue * toRange; };
const mapValue = ( value: number, fromMin: number, fromMax: number, toMin: number, toMax: number ) => { const fromRange = fromMax - fromMin; const toRange = toMax - toMin; const normalizedValue = (value - fromMin) / fromRange; return toMin + normalizedValue * toRange; };
const shiftX = mapValue(roundness, 0, 1, huggingX, roundX); const shiftY = mapValue(roundness, 0, 1, huggingY, roundY);
const shiftX = mapValue(roundness, 0, 1, huggingX, roundX); const shiftY = mapValue(roundness, 0, 1, huggingY, roundY);
export const generateSquirclePath = ( width: number, height: number, roundness: number = 0.5, precision: number = 2 ) => { const halfWidth = width / 2; const halfHeight = height / 2; const [huggingX, huggingY] = _getShiftForHuggingCurves(width, height); const [roundX, roundY] = _getShiftForRoundCurves(width, height); const shiftX = _mapValue(roundness, 0, 1, huggingX, roundX); const shiftY = _mapValue(roundness, 0, 1, huggingY, roundY); const curves = [ [ { x: 0, y: _setPrecision(halfHeight, precision) }, { x: 0, y: _setPrecision(halfHeight - shiftY, precision) }, { x: _setPrecision(halfWidth - shiftX, precision), y: 0 }, { x: _setPrecision(halfWidth, precision), y: 0 }, ], [ { x: _setPrecision(halfWidth, precision), y: 0 }, { x: _setPrecision(halfWidth + shiftX, precision), y: 0 }, { x: _setPrecision(width, precision), y: _setPrecision(halfHeight - shiftY, precision), }, { x: _setPrecision(width, precision), y: _setPrecision(halfHeight, precision), }, ], [ { x: _setPrecision(width, precision), y: _setPrecision(halfHeight, precision), }, { x: _setPrecision(width, precision), y: _setPrecision(halfHeight + shiftY, precision), }, { x: _setPrecision(halfWidth + shiftX, precision), y: _setPrecision(height, precision), }, { x: _setPrecision(halfWidth, precision), y: _setPrecision(height, precision), }, ], [ { x: _setPrecision(halfWidth, precision), y: _setPrecision(height, precision), }, { x: _setPrecision(halfWidth - shiftX, precision), y: _setPrecision(height, precision), }, { x: 0, y: _setPrecision(halfHeight + shiftY, precision) }, { x: 0, y: _setPrecision(halfHeight, precision) }, ], ]; return { pathString: _getSVGPathString(curves), curves, }; }; const _getShiftForHuggingCurves = (width: number, height: number) => { if (width === 0 && height === 0) return [0, 0] as const; if (width === 0) return [0, height / 2] as const; if (height === 0) return [width / 2, 0] as const; const ratio = width / height; const effectiveRatio = Math.max(ratio, 1 / ratio); const pullStrength = 0.5 / Math.pow(effectiveRatio, 0.2); const long = 1 + pullStrength / (3 * (1 - pullStrength)); const short = (1 + 2 * pullStrength) / (3 * pullStrength); const scaleX = width >= height ? long : short; const scaleY = width >= height ? short : long; return [(width / 2) * scaleX, (height / 2) * scaleY] as const; }; const _getShiftForRoundCurves = (width: number, height: number) => { const scale = 0.5522847498307933; // (4 / 3) * Math.tan(Math.PI / 8); return [(width / 2) * scale, (height / 2) * scale] as const; }; const _mapValue = ( value: number, fromMin: number, fromMax: number, toMin: number, toMax: number ) => { const fromRange = fromMax - fromMin; const toRange = toMax - toMin; const normalizedValue = (value - fromMin) / fromRange; return toMin + normalizedValue * toRange; }; const _getSVGPathString = (curves: { x: number; y: number }[][]) => [ `M ${curves[0][0].x} ${curves[0][0].y}`, ...curves.map( (curve) => `C ${curve[1].x} ${curve[1].y} ${curve[2].x} ${curve[2].y} ${curve[3].x} ${curve[3].y}` ), 'Z', ].join(' '); const _setPrecision = (value: number, precision: number) => parseFloat(value.toFixed(precision));
export const generateSquirclePath = ( width: number, height: number, roundness: number = 0.5, precision: number = 2 ) => { const halfWidth = width / 2; const halfHeight = height / 2; const [huggingX, huggingY] = _getShiftForHuggingCurves(width, height); const [roundX, roundY] = _getShiftForRoundCurves(width, height); const shiftX = _mapValue(roundness, 0, 1, huggingX, roundX); const shiftY = _mapValue(roundness, 0, 1, huggingY, roundY); const curves = [ [ { x: 0, y: _setPrecision(halfHeight, precision) }, { x: 0, y: _setPrecision(halfHeight - shiftY, precision) }, { x: _setPrecision(halfWidth - shiftX, precision), y: 0 }, { x: _setPrecision(halfWidth, precision), y: 0 }, ], [ { x: _setPrecision(halfWidth, precision), y: 0 }, { x: _setPrecision(halfWidth + shiftX, precision), y: 0 }, { x: _setPrecision(width, precision), y: _setPrecision(halfHeight - shiftY, precision), }, { x: _setPrecision(width, precision), y: _setPrecision(halfHeight, precision), }, ], [ { x: _setPrecision(width, precision), y: _setPrecision(halfHeight, precision), }, { x: _setPrecision(width, precision), y: _setPrecision(halfHeight + shiftY, precision), }, { x: _setPrecision(halfWidth + shiftX, precision), y: _setPrecision(height, precision), }, { x: _setPrecision(halfWidth, precision), y: _setPrecision(height, precision), }, ], [ { x: _setPrecision(halfWidth, precision), y: _setPrecision(height, precision), }, { x: _setPrecision(halfWidth - shiftX, precision), y: _setPrecision(height, precision), }, { x: 0, y: _setPrecision(halfHeight + shiftY, precision) }, { x: 0, y: _setPrecision(halfHeight, precision) }, ], ]; return { pathString: _getSVGPathString(curves), curves, }; }; const _getShiftForHuggingCurves = (width: number, height: number) => { if (width === 0 && height === 0) return [0, 0] as const; if (width === 0) return [0, height / 2] as const; if (height === 0) return [width / 2, 0] as const; const ratio = width / height; const effectiveRatio = Math.max(ratio, 1 / ratio); const pullStrength = 0.5 / Math.pow(effectiveRatio, 0.2); const long = 1 + pullStrength / (3 * (1 - pullStrength)); const short = (1 + 2 * pullStrength) / (3 * pullStrength); const scaleX = width >= height ? long : short; const scaleY = width >= height ? short : long; return [(width / 2) * scaleX, (height / 2) * scaleY] as const; }; const _getShiftForRoundCurves = (width: number, height: number) => { const scale = 0.5522847498307933; // (4 / 3) * Math.tan(Math.PI / 8); return [(width / 2) * scale, (height / 2) * scale] as const; }; const _mapValue = ( value: number, fromMin: number, fromMax: number, toMin: number, toMax: number ) => { const fromRange = fromMax - fromMin; const toRange = toMax - toMin; const normalizedValue = (value - fromMin) / fromRange; return toMin + normalizedValue * toRange; }; const _getSVGPathString = (curves: { x: number; y: number }[][]) => [ `M ${curves[0][0].x} ${curves[0][0].y}`, ...curves.map( (curve) => `C ${curve[1].x} ${curve[1].y} ${curve[2].x} ${curve[2].y} ${curve[3].x} ${curve[3].y}` ), 'Z', ].join(' '); const _setPrecision = (value: number, precision: number) => parseFloat(value.toFixed(precision));
import { SquircleContainer } from './SquircleContainer'; export const App = () => { return ( <div style={{ width: '1000px', height: '500px' }}> <SquircleContainer style={{ width: '100%', height: '100%' }} roundness={0.4} > <img src='https://picsum.photos/1000/500' alt='img' draggable={false} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> </SquircleContainer> </div> ); };
import { SquircleContainer } from './SquircleContainer'; export const App = () => { return ( <div style={{ width: '1000px', height: '500px' }}> <SquircleContainer style={{ width: '100%', height: '100%' }} roundness={0.4} > <img src='https://picsum.photos/1000/500' alt='img' draggable={false} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> </SquircleContainer> </div> ); };
import { useMemo, useRef, type HTMLAttributes } from 'react'; import { generateSquirclePath } from './generateSquirclePath'; import { useResizeObserver } from './useResizeObserver'; export const SquircleContainer = ( props: HTMLAttributes<HTMLDivElement> & { roundness: number } ) => { const containerRef = useRef<HTMLDivElement>(null); const dimensions = useResizeObserver(containerRef); const { pathString } = useMemo( () => generateSquirclePath(dimensions.width, dimensions.height), [dimensions.width, dimensions.height] ); return ( <div ref={containerRef} {...props} style={{ ...props.style, position: 'relative', }} > <div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', clipPath: `path("${pathString}")`, }} > {props.children} </div> </div> ); };
import { useMemo, useRef, type HTMLAttributes } from 'react'; import { generateSquirclePath } from './generateSquirclePath'; import { useResizeObserver } from './useResizeObserver'; export const SquircleContainer = ( props: HTMLAttributes<HTMLDivElement> & { roundness: number } ) => { const containerRef = useRef<HTMLDivElement>(null); const dimensions = useResizeObserver(containerRef); const { pathString } = useMemo( () => generateSquirclePath(dimensions.width, dimensions.height), [dimensions.width, dimensions.height] ); return ( <div ref={containerRef} {...props} style={{ ...props.style, position: 'relative', }} > <div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', clipPath: `path("${pathString}")`, }} > {props.children} </div> </div> ); };
import { type RefObject, useEffect, useState } from 'react'; interface Dimensions { width: number; height: number; } export function useResizeObserver(ref: RefObject<Element | null>): Dimensions { const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 }); useEffect(() => { if (!ref.current) return; const resizeObserver = new ResizeObserver(entries => { for (const entry of entries) { const { width, height } = entry.contentRect; setDimensions({ width, height }); } }); resizeObserver.observe(ref.current); return () => { resizeObserver.disconnect(); }; }, [ref]); return dimensions; }
import { type RefObject, useEffect, useState } from 'react'; interface Dimensions { width: number; height: number; } export function useResizeObserver(ref: RefObject<Element | null>): Dimensions { const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 }); useEffect(() => { if (!ref.current) return; const resizeObserver = new ResizeObserver(entries => { for (const entry of entries) { const { width, height } = entry.contentRect; setDimensions({ width, height }); } }); resizeObserver.observe(ref.current); return () => { resizeObserver.disconnect(); }; }, [ref]); return dimensions; }
Using squircle requires some compromises. We lose the ability to use CSS border
styles. If you want to draw an element's edge, you'll be forced to overlay an svg element on the container and define stroke
. box-shadow
won't work either; instead, you can use filter: drop-shadow()
.
import { SquircleContainer } from './SquircleContainer'; export const App = () => { return ( <div style={{ width: '1000px', height: '500px' }}> <SquircleContainer style={{ width: '100%', height: '100%', filter: 'drop-shadow(0 0 10px #000)', }} roundness={0.4} strokeWidth={5} strokeColor='#fff' > <img src='https://picsum.photos/1000/500' alt='img' draggable={false} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> </SquircleContainer> </div> ); };
import { SquircleContainer } from './SquircleContainer'; export const App = () => { return ( <div style={{ width: '1000px', height: '500px' }}> <SquircleContainer style={{ width: '100%', height: '100%', filter: 'drop-shadow(0 0 10px #000)', }} roundness={0.4} strokeWidth={5} strokeColor='#fff' > <img src='https://picsum.photos/1000/500' alt='img' draggable={false} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> </SquircleContainer> </div> ); };

I hope you'll successfully implement this component in your projects and contribute to building modern, beautiful interfaces.
Found a bug? Have suggestions? Let me know!
Happy coding!