4

Is there any way to make a squircle but also have the border, shadow, or inner glow also conform to the squircle shape?

Creating a squircle can be done with houdini via a CSS paintWorklet, but this has very poor support from browsers with usage being only 71.35% (2023) perhaps because of security vulnerabilities regarding the paint worklet.

Other alternatives include using an SVG <clipPath> with the squircle shape, but adding a squircle border has to be redrawn with the original clip path, making transformations difficult. The stroke path and clip path would need to be re-rendered as the element is scaled. The CSS properties of the element would need to be converted into a path data.

<svg xmlns="http://www.w3.org/2000/svg" width="220" height="220" viewBox="-10 -10 220 220">
  <defs>
    <clipPath id="squircle-clip">
      <path d="M20,0
        L180,0
        Q200,0 200,20
        L200,180
        Q200,200 180,200
        L20,200
        Q0,200 0,180
        L0,20
        Q0,0 20,0"
        style="vector-effect: non-scaling-stroke;" 
      />
    </clipPath>
  </defs>
  <rect x="0" y="0" width="200" height="200" fill="#222" clip-path="url(#squircle-clip)"
  />
  <path d="M20,0
    L180,0
    Q200,0 200,20
    L200,180
    Q200,200 180,200
    L20,200
    Q0,200 0,180
    L0,20
    Q0,0 20,0" 
    fill="none" stroke="#484848" stroke-width="2" style="vector-effect: non-scaling-stroke;"
  />
</svg>

Are there alternative methods to create a squircle with a border? Houdini is a hard solution to choose as it only has ~71% of all users, and without support for Safari (iOS & macOS) or Firefox.

j08691
  • 204,283
  • 31
  • 260
  • 272
Greg Wolff
  • 169
  • 10
  • To clarify, you're wanting to make an HTML element squircle-shaped in such a way that CSS properties like border and shadows respect the squircle? – Sean Jun 09 '23 at 12:25

2 Answers2

4

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.

herrstrietzel
  • 11,541
  • 2
  • 12
  • 34
-1

I Don't get your point 100%, but i tried as per my understanding. kindly check my code.

<svg xmlns="http://www.w3.org/2000/svg" width="220" height="220" viewBox="-10 -10 220 220">
  <defs>
    <clipPath id="squircle-clip">
      <path d="M20,0
      L20,0
    Q200,-10 200,150
        L200,180
        Q200,200 180,200
        L20,200
        Q0,200 0,180
        L0,20
        Q0,0 20,0"
        style="vector-effect: non-scaling-stroke;" 
      />
    </clipPath>
  </defs>
  <rect x="0" y="0" width="200" height="200" fill="#222" clip-path="url(#squircle-clip)"
  />
  <path d="M20,0
  L20,0
    Q200,-10 200,150
    L200,180
    Q200,200 180,200
    L20,200
    Q0,200 0,180
    L0,20
    Q0,0 20,0" 
    fill="none" stroke="#484848" stroke-width="4" style="vector-effect: non-scaling-stroke;"
  />
</svg>
    
Prince
  • 95
  • 4
  • I think they're wanting to make an HTML element squircle-shaped in such a way that CSS properties like border and shadows respect the squircle. They're not looking for an SVG. – Sean Jun 09 '23 at 12:25