Clip paths (as well as masks) will clip strokes and filters
Using clip-paths works well if you don't need any strokes or filters/effects like dropshadow.
If your ultimate goal is to create an iOS like icon svg might be your best option:
.resize {
border: 1px solid #ccc;
resize: both;
overflow: auto;
width: 50%;
max-width: 50%;
}
svg {
width: 100%;
}
.icon {
fill: orange;
stroke: #000;
stroke-width: 2px;
filter: drop-shadow(5px 5px 2px rgba(0, 0, 0, 0.75));
}
<h3>Resize me</h3>
<div class="resize">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120">
<path d="M 60 10 h 0 c 35.385 0 50 14.615 50 50 v 0 c 0 35.385 -14.615 50 -50 50 h 0 c -35.385 0 -50 -14.615 -50 -50 v 0 c 0 -35.385 14.615 -50 50 -50" />
</svg>
</div>
The above squircle is generated with a simple helper I created:
codepen "Squircle generator (clothoid rounded corners)"
If you need to create a squircle dynamically (e.g in a drawing app) you could sync an svg <rect>
with an dynamically updated <path>
element:
Synced <path>
dynamically updated on resize
You could create a <path>
as a "clone" of the rounded <rect>
element sharing its width, height and other properties.
The rect is hidden but responds to transformations.
Once the rect is transformed, the <path>
d attribute gets recalculated according to the current rect dimensions.
addClothoidPaths(2.5, 0.9, true);
document.querySelectorAll(".inputs").forEach((input) => {
input.addEventListener("input", (e) => {
let r = +inputBorderRadius.value;
let tension = +inputTension.value;
let cubic = +document.querySelector('input[name="bezierType"]:checked')
.value;
addClothoidPaths(r, tension, cubic);
});
});
function addClothoidPaths(borderRadius = 2.5, tension = 0.9, cubic = true) {
const ns = "http://www.w3.org/2000/svg";
let rects = document.querySelectorAll("rect");
rects.forEach((rect, i) => {
let svg = rect.closest("svg");
// create clothoid rounded path
let rectPathGroup = svg.querySelector(".rectGroup");
let rectPath = svg.querySelector(".rectPath" + i);
if (!rectPathGroup) {
rectPathGroup = document.createElementNS(ns, "g");
rectPathGroup.classList.add("rectGroup");
svg.append(rectPathGroup);
rectPath = document.createElementNS(ns, "path");
rectPath.classList.add("rectPath" + i);
rectPathGroup.append(rectPath);
}
//console.log(rectPath)
/**
* copy rect attributes
*/
const setAttributes = (el, attributes, exclude = []) => {
for (key in attributes) {
if (exclude.indexOf(key) === -1) {
el.setAttribute(key, attributes[key]);
}
}
};
const getAttributes = (el) => {
let attArr = [...el.attributes];
let attObj = {};
attArr.forEach(function (att) {
attObj[att.nodeName] = att.nodeValue;
});
return attObj;
};
//exclude attributes not needed for paths
let exclude = ["x", "y", "r", "rx", "ry", "height", "width", "id"];
// copy attributes to path and set pathData
let attributes = getAttributes(rect);
setAttributes(rectPath, attributes, exclude);
//hide rect
rect.style.visibility = "hidden";
let d = updateClothoid(rect, borderRadius, tension);
rectPath.setAttribute("d", d);
rectPath.style.visibility = "visible";
let resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
let d = updateClothoid(entry.target, borderRadius, tension, cubic);
rectPath.setAttribute("d", d);
updateOutput();
});
});
// Observe one or multiple elements
resizeObserver.observe(rect);
});
}
function updateOutput() {
output.value = new XMLSerializer().serializeToString(svg);
}
function updateClothoid(rect, borderRadius, tension, cubic = true) {
let x = rect.x.baseVal.value;
let y = rect.y.baseVal.value;
let w = rect.width.baseVal.value;
let h = rect.height.baseVal.value;
let r = rect.rx.baseVal.value;
let rC = r * borderRadius;
let lineHLength = w - rC * 2;
let lineVLength = h - rC * 2;
let d = "";
// prevent border radius smaller than half width
if (rC > w / 2 || rC > h / 2) {
rC = Math.min(...[w, h]) / 3;
lineHLength = w - rC * 2;
lineVLength = h - rC * 2;
}
if (cubic) {
d = `
M ${x + rC} ${y}
h ${lineHLength}
c ${rC * tension} 0
${rC} ${rC * (1 - tension)}
${rC} ${rC}
v ${lineVLength}
c 0 ${rC * tension}
-${rC * (1 - tension)} ${rC}
-${rC} ${rC}
h -${lineHLength}
c -${rC * tension} 0
-${rC} -${rC * (1 - tension)}
-${rC} -${rC}
v-${lineVLength}
c 0 -${rC * tension}
${rC * (1 - tension)} -${rC}
${rC} -${rC}`;
}
// quadratic border smoothing
else {
d = `
M ${x + rC} ${y}
h ${lineHLength}
q ${rC} 0
${rC} ${rC}
v ${lineVLength}
q 0 ${rC}
-${rC} ${rC}
h -${lineHLength}
q -${rC} 0
-${rC} -${rC}
v -${lineVLength}
q 0 -${rC}
${rC} -${rC}`;
}
return d.replace(/[\n\r\t]/g, "").replace(/\s{2,}/g, " ");
}
.resize {
border: 1px solid #ccc;
resize: both;
overflow: auto;
width: 50%;
max-width: 50%;
}
svg {
display: block;
width: 100%;
height: 100%;
}
textarea {
width: 100%;
min-height: 20em;
}
<p>Border-radius<input class="inputs" id="inputBorderRadius" type="range" min="1" max="5" step="0.1"></p>
<p>Tension (only for cubic)<input class="inputs" id="inputTension" type="range" min="0.5" max="1" step="0.1"></p>
<p><label> <input class="inputs" type="radio" name="bezierType" value="1" checked> Cubic</label> <label> <input class="inputs" type="radio" name="bezierType" value="0"> Quadratic</label> </p>
<h3>Resize me</h3>
<div class="resize">
<svg id="svg">
<rect id="rect" x="10%" y="10%" width="80%" height="80%" rx="10" fill="#ccc" stroke="#000" stroke-width="10" transform="rotate(0)" transform-orgin="center" />
</svg>
</div>
<fieldset>
<legend>Output</legend>
<textarea id="output"></textarea>
</fieldset>
Calculate pseudo clothoid rounded corners
Based on the rect initial rx
border radius attribute we can calculate the quadratic or cubic curve segments like so:
function updateClothoid(rect, borderRadius, tension, cubic = true) {
let x = rect.x.baseVal.value;
let y = rect.y.baseVal.value;
let w = rect.width.baseVal.value;
let h = rect.height.baseVal.value;
let r = rect.rx.baseVal.value;
let rC = r * borderRadius;
// horizontal and vertical line segments between curves
let lineHLength = w - rC * 2;
let lineVLength = h - rC * 2;
let d = "";
// prevent border radius smaller than half width
if (rC > w / 2 || rC > h / 2) {
rC = Math.min(...[w, h]) / 3;
lineHLength = w - rC * 2;
lineVLength = h - rC * 2;
}
if (cubic) {
d = `
M ${x + rC} ${y}
h ${lineHLength}
c ${rC * tension} 0
${rC} ${rC * (1 - tension)}
${rC} ${rC}
v ${lineVLength}
c 0 ${rC * tension}
-${rC * (1 - tension)} ${rC}
-${rC} ${rC}
h -${lineHLength}
c -${rC * tension} 0
-${rC} -${rC * (1 - tension)}
-${rC} -${rC}
v-${lineVLength}
c 0 -${rC * tension}
${rC * (1 - tension)} -${rC}
${rC} -${rC}`;
}
// quadratic border smoothing
else {
d = `
M ${x + rC} ${y}
h ${lineHLength}
q ${rC} 0
${rC} ${rC}
v ${lineVLength}
q 0 ${rC}
-${rC} ${rC}
h -${lineHLength}
q -${rC} 0
-${rC} -${rC}
v -${lineVLength}
q 0 -${rC}
${rC} -${rC}`;
}
// remove whitespace
return d.replace(/[\n\r\t]/g, "").replace(/\s{2,}/g, " ");
}
Higher rC
will increase the initial border radius for a smoother curve.
Cubic Béziers allow more control of the curvature.
Higher tension
value will "pull" the control point to the corners resulting in a visually smaller border-radius.
Both options (quadratic and cubic) will produce a smoother transition between straight lines and curves than default border-radius methods based on adding circle arcs.
let d = updateClothoid(rect, 2.5, 0.9, true);
path.setAttribute('d', d)
function updateClothoid(rect, borderRadius, tension, cubic = true) {
let x = rect.x.baseVal.value;
let y = rect.y.baseVal.value;
let w = rect.width.baseVal.value;
let h = rect.height.baseVal.value;
let r = rect.rx.baseVal.value;
let rC = r * borderRadius;
// horizontal and vertical line segments between curves
let lineHLength = w - rC * 2;
let lineVLength = h - rC * 2;
let d = "";
// prevent border radius smaller than half width
if (rC > w / 2 || rC > h / 2) {
rC = Math.min(...[w, h]) / 3;
lineHLength = w - rC * 2;
lineVLength = h - rC * 2;
}
if (cubic) {
d = `
M ${x + rC} ${y}
h ${lineHLength}
c ${rC * tension} 0
${rC} ${rC * (1 - tension)}
${rC} ${rC}
v ${lineVLength}
c 0 ${rC * tension}
-${rC * (1 - tension)} ${rC}
-${rC} ${rC}
h -${lineHLength}
c -${rC * tension} 0
-${rC} -${rC * (1 - tension)}
-${rC} -${rC}
v-${lineVLength}
c 0 -${rC * tension}
${rC * (1 - tension)} -${rC}
${rC} -${rC}`;
}
// quadratic border smoothing
else {
d = `
M ${x + rC} ${y}
h ${lineHLength}
q ${rC} 0
${rC} ${rC}
v ${lineVLength}
q 0 ${rC}
-${rC} ${rC}
h -${lineHLength}
q -${rC} 0
-${rC} -${rC}
v -${lineVLength}
q 0 -${rC}
${rC} -${rC}`;
}
// remove whitespace
return d.replace(/[\n\r\t]/g, "").replace(/\s{2,}/g, " ");
}
svg {
display: block;
width: 20em;
border: 1px solid #ccc
}
<svg id="svg" viewBox="0 0 100 100">
<rect id="rect" x="10%" y="10%" width="80%" height="80%" rx="10" fill="none" stroke="#ccc" stroke-width="0.5" transform="rotate(0)" transform-orgin="center" />
<path id="path" fill="none" stroke="red" stroke-width="0.75"/>
</svg>
CSS aproach: Wrapped squircle with CSS clip-path
You might also use a css squircle generator like "CSS Clothoid Corners".
:root{
--clip: polygon(45.837405% 0%,
calc(100% - 45.837405%) 0%,
calc(100% - 41.024763%) 0.022716%,
calc(100% - 36.21469%) 0.166797%,
calc(100% - 31.418198%) 0.543282%,
calc(100% - 26.661607%) 1.261583%,
calc(100% - 22.002771%) 2.456312%,
calc(100% - 17.530819%) 4.217852%,
calc(100% - 13.363987%) 6.605826%,
calc(100% - 9.657177%) 9.657177%,
calc(100% - 6.605826%) 13.363987%,
calc(100% - 4.217852%) 17.530819%,
calc(100% - 2.456312%) 22.002771%,
calc(100% - 1.261583%) 26.661607%,
calc(100% - 0.543282%) 31.418198%,
calc(100% - 0.166797%) 36.21469%,
calc(100% - 0.022716%) 41.024763%,
calc(100% - 0.022716%) calc(100% - 41.024763%),
calc(100% - 0.166797%) calc(100% - 36.21469%),
calc(100% - 0.543282%) calc(100% - 31.418198%),
calc(100% - 1.261583%) calc(100% - 26.661607%),
calc(100% - 2.456312%) calc(100% - 22.002771%),
calc(100% - 4.217852%) calc(100% - 17.530819%),
calc(100% - 6.605826%) calc(100% - 13.363987%),
calc(100% - 9.657177%) calc(100% - 9.657177%),
calc(100% - 13.363987%) calc(100% - 6.605826%),
calc(100% - 17.530819%) calc(100% - 4.217852%),
calc(100% - 22.002771%) calc(100% - 2.456312%),
calc(100% - 26.661607%) calc(100% - 1.261583%),
calc(100% - 31.418198%) calc(100% - 0.543282%),
calc(100% - 36.21469%) calc(100% - 0.166797%),
calc(100% - 41.024763%) calc(100% - 0.022716%),
calc(100% - 45.837405%) 100%,
45.837405% 100%,
41.024763% calc(100% - 0.022716%),
36.21469% calc(100% - 0.166797%),
31.418198% calc(100% - 0.543282%),
26.661607% calc(100% - 1.261583%),
22.002771% calc(100% - 2.456312%),
17.530819% calc(100% - 4.217852%),
13.363987% calc(100% - 6.605826%),
9.657177% calc(100% - 9.657177%),
6.605826% calc(100% - 13.363987%),
4.217852% calc(100% - 17.530819%),
2.456312% calc(100% - 22.002771%),
1.261583% calc(100% - 26.661607%),
0.543282% calc(100% - 31.418198%),
0.166797% calc(100% - 36.21469%),
0.022716% calc(100% - 41.024763%),
0.022716% 41.024763%,
0.166797% 36.21469%,
0.543282% 31.418198%,
1.261583% 26.661607%,
2.456312% 22.002771%,
4.217852% 17.530819%,
6.605826% 13.363987%,
9.657177% 9.657177%,
13.363987% 6.605826%,
17.530819% 4.217852%,
22.002771% 2.456312%,
26.661607% 1.261583%,
31.418198% 0.543282%,
36.21469% 0.166797%,
41.024763% 0.022716%,
45.837405% 0%);
}
.cloth-wrp{
position: relative;
display: inline-block;
width:50%;
padding: 5px;
filter: drop-shadow(5px 5px 5px rgba(0,0,0,0.5));
}
.cloth-wrp:before{
content:'';
display:block;
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
width:100%;
height:100%;
background:#000;
clip-path: var(--clip)
}
.clothoid-corner {
display: flex;
align-items: center;
justify-content: center;
background-color:orange;
width:100%;
aspect-ratio: 1/1;
}
.clipped{
clip-path: var(--clip)
}
<div class="cloth-wrp ">
<div class="clothoid-corner clipped">
<p>Test clothoid</p>
</div>
</div>
This clip path is actually a polygon approximation.
We need to wrap the squircle in a relatively positioned parent div.
This wrapper introduces a pseudo element – clipped with the same clip-path.
The pseudo elements has a background color which will result in the final pseudo stroke color.
The stroke-width is defined by a padding applied to the wrapping element.