Squircle - zaokrąglaj krawędzie inaczej niż wszyscy
15 maja 2025 (Updated: 19 czerwca 2025)Squircle to świetna alternatywa dla zwykłych zaokrągleń. W tym artykule pokażę Ci, jak uzyskać ten efekt i wykorzystać go w swoich projektach.
Jeśli przyszedłeś tylko po gotowe rozwiązanie i nie interesują Cię szczegóły, możesz pobrać gotowy projekt, przejść do projektu na GitHub lub zainstalować paczkę npm:
npm install batstack-squircle
npm install batstack-squircle
yarn add batstack-squircle
yarn add batstack-squircle
Skoro tu trafiłeś, to pewnie już słyszałeś o tym ciekawym kształcie, jakim jest squircle.
Został rozpowszechniony głównie przez Apple, który wykorzystuje go w swoich interfejsach. Powód jest prosty - bardziej naturalne, miękkie zaokrąglenia wyglądają estetycznie i subtelnie wyróżniają się na tle tych prostych, jak od linijki.
W interfejsie, użycie go ma głównie sens w przypadku obrazów. CSS niestety nie oferuje bezpośredniej możliwości tworzenia takich kształtów. Można jednak dość łatwo osiągnąć taki efekt i w tym artykule pokażę Ci, jak to zrobić.
Jest to najprostszy przypadek. Gdy element ma określone wymiary, możesz wygenerować ścieżkę SVG i użyć jej jako clip-path
, aby wyciąć obraz w kształcie squircle.
<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"); }

Jeśli zastanawiasz się, skąd wziać tę ścieżkę, z pomocą przyjdzie Ci moje narzędzie online, które znajdziesz tu: Generator SVG squircle.
Tu sprawa robi się trochę bardziej skomplikowana. Nie możemy użyć wcześniej wygenerowanej ścieżki - musimy generować ją na bieżąco, gdy element zmienia wymiary.
Ścieżka określona jest koordynatami punktów (krzywymi Béziera, o tym więcej później). Nie możemy w niej użyć wartości procentowych. Będziemy więc zmuszeni do obserwowania zmian wymiaru elementu, aby uzyskać wartości w pikselach.
W JavaScript mamy do dyspozycji ResizeObserver, który pozwala nam obserwować takie zmiany wymiarów.
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> </> ); };
Squircle wygenerujemy z pomocą krzywych Béziera, które łatwo zdefiniować w SVG. Jest to też najpopularniejszy sposób rysowania krzywych w grafice wektorowej.
Krzywa Béziera składa się z punktu początkowego, punktu końcowego i punktów kontrolnych, definiujących jej kształt i może być różnego stopnia. Stopień określa liczbę punktów kontrolnych. W tym projekcie użyjemy krzywych drugiego stopnia, czyli zdefinujemy dwa punkty kontrolne.
W poniższym przykładzie punkt początkowy i końcowy są zdefiniowane jako { x: 0, y: 0 }
i { x: 0, y: 200 }
, a punkty kontrolne możesz dowolnie przesuwać, aby zobaczyć, jak zmienia się kształt krzywej.
Nasz kształt składa z czterech takich krzywych, rozciągniętych pomiędzy środkami krawędzi elementu. Dla uproszczenia, umieśćmy punkty kontrolne w rogach elementu.
Result:
[ [ // curve 1 { x: 0, y: 150} // punkt początkowy { x: 0, y: 0} // punkt kontrolny 1 { x: 0, y: 0} // punkt kontrolny 2 { x: 125, y: 0} // punkt końcowy ], // 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} // punkt początkowy { x: 0, y: 0} // punkt kontrolny 1 { x: 0, y: 0} // punkt kontrolny 2 { x: 125, y: 0} // punkt końcowy ], // 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
Jeśli jesteś spostrzegawczy, możesz zauważyć, że dla każdej krzywej, generujemy po 4 punkty, a w wyjściowym stringu używamy tylko 3. Wynika to z tego, jak rysowane są ścieżki w SVG. Rysując kolejną krzywą, nie zaczynamy "od zera", a od miejsca, gdzie skończyliśmy rysować poprzednią. Stąd pierwszym punktem nowej krzywej jest punkt końcowy poprzedniej.
// cp1X, cp1Y - punkt kontrolny 1 // cp2X, cp2Y - punkt kontrolny 2 // endX, endY - punkt końcowy C cp1X cp1Y cp2X cp2Y endX endY
// cp1X, cp1Y - punkt kontrolny 1 // cp2X, cp2Y - punkt kontrolny 2 // endX, endY - punkt końcowy C cp1X cp1Y cp2X cp2Y endX endY
M 0 150 // przesuń kursor na pozycję [0, 150] C 0 0 0 0 125 0 // rysuj krzywą Béziera od [0, 150] do [125, 0] C 250 0 250 0 250 150 // rysuj krzywą Béziera od [125, 0] do [250, 150] C 250 300 250 300 125 300 // rysuj krzywą Béziera od [250, 150] do [125, 300] C 0 300 0 300 0 150 // rysuj krzywą Béziera od [125, 300] do [0, 150] Z // zakończ rysowanie
M 0 150 // przesuń kursor na pozycję [0, 150] C 0 0 0 0 125 0 // rysuj krzywą Béziera od [0, 150] do [125, 0] C 250 0 250 0 250 150 // rysuj krzywą Béziera od [125, 0] do [250, 150] C 250 300 250 300 125 300 // rysuj krzywą Béziera od [250, 150] do [125, 300] C 0 300 0 300 0 150 // rysuj krzywą Béziera od [125, 300] do [0, 150] Z // zakończ rysowanie
Zapoznaj się teraz z implementacją i przykładem użycia.

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> </> ); };
To rozwązanie jak najbardziej działa, ma jednak sporą wadę - nie pozwala na ustawienie poziomu zaokrąglenia. Stała pozycja punktów kontrolnych sprawia również, że zaokrąglenie jest zbyt duże, gdy element ma wysoki współczynnik proporcji, tzn. gdy jest np. bardzo niski, ale szeroki.
Jeśli jednak te ograniczenia nie są dla Ciebie istotne, możesz użyć tego rozwiązania. Będzie ono działać dobrze w większości przypadków i jest bardziej wydajne niż pełna implementacja.
Pozycja punktów kontrolnych powinna być dynamiczna, aby dawać zbliżony poziom zaokrąglenia, bez względu na wymiary elementu. Aby sterować zaokrągleniem, wprowadzimy dodatkowy parametr roundness
do funkcji. Będzie on sterował przesunięciem punktów kontrolnych.
Krzywe będą tworzyć bardziej okrągły kształt, gdy punkty kontrolne będą przesunięte bliżej punktów początkowych i końcowych na osi stycznej. Gdy będą się oddalać, efekt będzie bardziej ostry.
To, że krzywa wychodzi poza element, nie jest błędem. Ponieważ używamy jej do wycięcia kształtu, efektem w tym przypadku będzie po prostu niewycięty element, czyli brak zaokrąglenia.
roundness
z założenia przyjmuje wartości od 0
do 1
.
0
oznacza całkowicie ostry kształt
1
oznacza kształt koła
Zacznijmy od prostszego przypadku - uzyskania kształtu zbliżonego do koła. Krzywe Béziera nie są w stanie stworzyć idealnego okręgu. Można jednak tak ustawić punkty kontrolne, aby uzyskać zbliżony efekt. Wystarczy zastosować stałą:
Jest to współczynnik, który określa, jak daleko od punktu początkowego i końcowego na osi stycznej muszą znajdować się punkty kontrolne dla elementu o wymiarach 1x1, aby odwzorować ćwiartkę okręgu.
Oznacza to, że przesunięcie dla dowolnych wymiarów to:
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; };
Jeśli chcemy uzyskać brak zaokrąglenia, sprawa może wydawać się prosta - wystarczy ustawić wartość przesunięcia na tyle dużą, aby krzywa była poza elementem i go nie wycinała. Niestety nie jest to tak trywialne.
Co prawda chcemy, aby krzywa wychodziła poza element, ale była też możliwie blisko krawędzi. Idealnie, gdyby dotykała narożników. Dzięki temu, nawet niskie wartości roundness
, typu 0.1
, będą dawać widoczny efekt, wycinając niewielki fragment rogów.
Funkcja, która realizuje to zadanie i działa dla elementu o dowolnych wymiarach, jest trochę bardziej skomplikowana. Dostosowuje siłę przesunięcia zależnie od proporcji elementu — im bardziej kształt odbiega od kwadratu, tym dalej wysuwane są punkty kontrolne — w taki sposób, by krzywa zawsze "obejmowała" narożniki.
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; };
W wyniku użycia powyższych funkcji, otrzymujemy przesunięcie w pikselach. Jako argument roundness
podajemy jednak wartość w zakresie od 0 do 1. Musimy więc przekształcić wartość zokrąglenia na odpowiednią skalę, będącą przesunięciem w pikselach. W tym celu użyjemy funkcji mapValue
, która przekształca wartość z jednego zakresu na drugi.
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; }
Użycie squircle wymaga pewnych kompromisów. Tracimy możliwość używania stylów CSS border
. Jeśli chcesz narysować krawędź elementu, będziesz zmuszony nałożyć na kontener element svg i zdefiniować stroke
. Nie zadziała również box-shadow
, zamiast tego możesz użyć 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> ); };

Mam nadzieję, że z powodzeniem wdrożysz ten komponent w swoich projektach i dołożysz cegiełkę w budowaniu nowoczesnych, pięknych interfejsów.
Znalazłeś błąd? Masz sugestie? Daj mi znać!
Powodzenia w kodowaniu!