Search code examples
javascripthtmlsvgcode-reuse

HTML SVG reuse a group <g> with <use> and change attributes of inner elements individually for each instance


So i'd like to reuse a grouped svg shape and change one attribute of one of the elements inside of the group individually for each instance. The following simplified example creates a second circle with a rectangle inside. I now want to change the "width" attribute of the "my-rect" rectangle individually for each of the shapes with javascript. Using the id "my-rect" will change the width of both rectangles, but I want to change only one.

My goal (if my approach is nonsense): I have to draw multiple of these shapes and the only thing that differs is the position and the width of the rectangle.

<svg height="1000" width="1000">
  <a transform="translate(110,110)">
    <g id="my-group">
      <g>
        <circle r="100" fill="#0000BF" stroke="black" stroke-width="2" fill-opacity="0.8"></circle>
      </g>
      <g>
        <rect id="my-rect" y="-50" height="100" x="-50" width="50">
        </rect>
      </g>
    </g>
  </a>
  <use xlink:href="#my-group" x="340" y="110"/>
</svg>


Solution

  • With a bit of trickery, it is possible. You have to take advantage of CSS inheritance to get some property value inside a shadow element. In this case, it will be custom variables that will be used to scale and position the rectangle.

    The markup has to be rewritten a bit for this. First, you write your group inside a <defs> element, making it a template for reuse, but not rendered by itself. Second, the rectangle is placed inside a nested <svg overflow="visible"> element. Giving this element the x/y coordinates and leaving them at 0 for the <rect> element makes it easier to track where the left side of the rectangle will end up after a transforming operation.

    Now the width change of the rect is achieved with a scaleX() transformation plus a translate() for the position. This must be in CSS transform syntax. Using a transform attribute would not work (yet). Therefore, we also need a transform-origin property, set to the left side of the enclosing <svg> element.

    Instead of writing a concrete value for the scaling, the value is expressed as a variable with the default value 1: var(--scale, 1); same for the positional values. The value for the variable is set in a style attribute for each <use> element separately: style="--scale:2;--posX:20px; --posY:-10px". Note the need for writing px units!

    #my-rect {
        transform-origin: left top;
        transform: translate(var(--posX, 0), var(--posY, 0)) scaleX(var(--scale, 1));
    }
    <svg height="1000" width="1000">
      <defs>
        <g id="my-group">
          <g>
            <circle r="100" fill="#0000BF" stroke="black" stroke-width="2" fill-opacity="0.8"></circle>
          </g>
          <svg x="-50" y="-50" overflow="visible">
            <rect id="my-rect" height="100" width="50">
            </rect>
          </svg>
        </g>
      </defs>
      <use xlink:href="#my-group" x="110" y="110" style="--scale:1"/>
      <use xlink:href="#my-group" x="340" y="110" style="--scale:2;--posX:20px; --posY:-10px"/>
    </svg>