skip to Main Content

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.

2

Answers


  1. This can be done easy with a div and border-radius + border , you can even add a glow effect to it

    div {
      height: 200px;
      width: 200px;
      background-color: red;
      border: 5px solid black;
      border-radius: 50%;
    }
    
    .glow {
      box-shadow: inset 0 0 50px black;
    }
    <div></div>
    <div class="glow"></div>
    Login or Signup to reply.
  2. Squircle based on <rect> with <rx>

    You might use a <rect> element with a border radius applied via rx, ry attributes.

    You can easily change width and height of a <rect> while maintaining the desired border-radius.

    Convert <rect> to <path>

    Converting it to a <path> can be achieved e.g using getPathData() polyfill.

    // getTransformToElement polyfill
    SVGElement.prototype.getTransformToElement =
      SVGElement.prototype.getTransformToElement ||
      function (toElement) {
        return toElement.getScreenCTM().inverse().multiply(this.getScreenCTM());
      };
    
    convertTransformations(svg);
    
    function convertTransformations(svg, precision = 3) {
      // query all svg visible elements
      let svgEls = [
        "path",
        "circle",
        "rect",
        "polygon",
        "polyline",
        "line",
        "ellipse"
      ];
      let svgSel = svg.querySelectorAll(svgEls);
      // apply transforms to each svg geometry element
      svgSel.forEach((el) => {
        //let matrix = getElementTransform(el, svg);
        let matrix = el.getTransformToElement(svg);
        let {a,b,c,d,e,f} = matrix;
        let matrixStr = [a,b,c,d,e,f]
          .map((val) => {
            return +val.toFixed(precision);
          })
          .join(",");
        // skip non transformed elements
        if (matrixStr !== "1,0,0,1,0,0") {
          el.setAttribute("transform", `matrix(${matrixStr})`);
          el.style.removeProperty("transform");
    
          // remove all transform origins
          el.style.removeProperty("transform-origin");
          el.removeAttribute("transform-origin");
    
          //convert transforms to hardcoded coordinates
          // convert primitives to paths
          let pathData = el.getPathData({
            normalize: true
          });
    
          pathData.forEach((command, d) => {
            let values = command.values;
            // loop through coordinates:
            for (let v = 0; v < values.length - 1; v += 2) {
              let [x, y] = [values[v], values[v + 1]];
              let pt = svg.createSVGPoint();
              pt.x = x;
              pt.y = y;
              // change coordinates by matrix transform
              let pTrans = pt.matrixTransform(matrix);
              // save coordinates to pathdata array
              pathData[d]["values"][v] = +(pTrans.x).toFixed(precision);
              pathData[d]["values"][v + 1] = +(pTrans.y).toFixed(precision);
            }
          });
    
          //check if conversion is needed for primitives (rect, circle, polygons etc.)
          if (el.nodeName.toLowerCase() != "path") {
            let newPath = document.createElementNS(
              "http://www.w3.org/2000/svg",
              "path"
            );
            let atts = [...el.attributes];
            let excludedAtts = [
              "d",
              "x",
              "y",
              "x1",
              "y1",
              "x2",
              "y2",
              "cx",
              "cy",
              "r",
              "rx",
              "ry",
              "points",
              "height",
              "width"
            ];
            for (let a = 0; a < atts.length; a++) {
              let att = atts[a];
              if (excludedAtts.indexOf(att.nodeName) === -1) {
                let attrName = att.nodeName;
                let attrValue = att.nodeValue;
                newPath.setAttribute(attrName, attrValue + "");
              }
            }
            el.replaceWith(newPath);
            el = newPath;
            el.setPathData(pathData);
            el.removeAttribute("transform");
            el.style.removeProperty("transform");
          }
        }
      });
    
      // remove group transformations
      let groups = svg.querySelectorAll("g");
      groups.forEach((group) => {
        group.removeAttribute("transform");
        group.style.removeProperty("transform");
        group.removeAttribute("transform-origin");
        group.style.removeProperty("transform-origin");
        // remove empty style attributes
        if (!group.getAttribute("style")) {
          group.removeAttribute("style");
        }
      });
      
      let svgMarkup = new XMLSerializer().serializeToString(svg);
      svgMarkupOut.value = svgMarkup;
      
      
    }
    body{
      padding:3em;
    }
    
    svg{
      overflow:visible;
      width:10em;
      border:1px solid #ccc;
      margin-bottom:1em;
    }
    
    textarea{
      width:100%;
      min-height:20em;
    }
    <svg id="svg" xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 220 220">
      <rect id="rect" x="10" y="10" width="200" height="200" rx="20" stroke="red" stroke-width="5" transform="rotate(22.5 110 110)"/>
    </svg>
    
    <textarea id="svgMarkupOut"></textarea>
    
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/path-data-polyfill.min.js"></script>

    See also "SVG – Convert all shapes/primitives to " for more details.

    Converting transformations to pathdata

    First we need to get the rect’s transform "total" matrix i.e respecting all parent transformation (e.g from <g>) elements.

    I’m using a getTransformToElement polyfill/helper method

    SVGElement.prototype.getTransformToElement =
      SVGElement.prototype.getTransformToElement ||
      function (toElement) {
        return toElement.getScreenCTM().inverse().multiply(this.getScreenCTM());
      };
    

    Once we have the retrieved the rect’s pathdata, we can recalculate all command coordinates via matrixTransform().

    It’s crucial to normalize the pathData first i.e we need

    • absolute coordinates
    • shorthand commands must be converted to longhand equivalents (e.g. S to C)
    • A (arc) commands must be converted to cubic C béziers (it might be possible to write a special conversion keeping arc commands – but it’s more complicated especially when it comes to transformations like skew.

    Normalizing can be done by adding the normalize parameter like so:

    getPathData({normalize: true});
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search