Search code examples
svgtransparencyclickablepointer-events

Restrict the clickable area of an SVG to its visiblePainted contents


I have a simple SVG that contains a triangle and toggles its fill color when it's clicked:

const toggleFill = (element) => {
  if (element.style.fill === 'silver') {
    element.style.fill = 'grey';
  } else {
    element.style.fill = 'silver';
  }
};
const svg = document.querySelector('svg');
svg.addEventListener('click', () => toggleFill(svg));
svg {
  height: 70px;
  width: 80px;
}
<svg viewBox="0 0 90 78" style="fill: silver">
  <polygon points="0 78, 45 0, 90 78" stroke="black"/>
</svg>

The problem is that clicking anywhere within the bounding box of the SVG (outside of the triangle) also triggers the click event. I'd like to restrict it to only toggle the fill color when the triangle itself is clicked, not its transparent background.

According to this related question my desired behavior should be the default behavior, since pointer-events is supposed to default to the visiblePainted behavior. Nonetheless, that's not what I see in either Firefox or Chrome, and even setting it explicitly has no effect.


Solution

  • The problem here is that the <svg> element is part of a HTML page. There, the behavior of hit-testing is undefined:

    This specification does not define the behavior of pointer events on the outermost svg element for SVG images which are embedded by reference or inclusion within another document, e.g., whether the outermost svg element embedded in an HTML document intercepts mouse click events; future specifications may define this behavior, but for the purpose of this specification, the behavior is implementation-specific.

    If you look up pointer-events in a HTML context, you will find more or less nothing:

    While this property modifies the normal behavior of hit-testing, this normal hit-testing is currently not specified. There is broad interoperability about the seemingly obvious parts of this problem, but there are countless nuances and corner cases that would greatly benefit from a detailed specification. The CSS-WG would hugely appreciate help with writing such a specification.

    So for now it seems browsers treat the outermost <svg> element like any any other box and click events are captured within the whole border-box.

    But this is only true for the outermost <svg> element. Nest another <svg> inside, (or better a <g> element) and attach the event listener to that, and the expected default behavior of paintedVisible is restored:

    const toggleFill = (element) => {
      if (element.style.fill === 'silver') {
        element.style.fill = 'grey';
      } else {
        element.style.fill = 'silver';
      }
    };
    const g = document.querySelector('svg > g');
    g.addEventListener('click', () => toggleFill(g));
    svg {
      height: 70px;
      width: 80px;
    }
    <svg viewBox="0 0 90 78">
      <g style="fill: silver">
        <polygon points="0 78, 45 0, 90 78" stroke="black"/>
      </g>
    </svg>