skip to Main Content

I try to create a HTML/CSS/JS funnel graph, with a simple front-end structure, in order to do so, I place funnel segments as a flex row:

document.addEventListener( 'DOMContentLoaded', function() {
    let funnels = document.getElementsByClassName( 'funnel' );
    
    for( let i = 0; i < funnels.length; i++ ) {
        let segments = funnels[i].children[1].children;
        
        for( let j = 0; j < segments.length; j++ ) {
            const from = parseFloat( segments[j].getAttribute( 'data-from' ) );
            const to = parseFloat( segments[j].getAttribute( 'data-to' ) );
            const difference = ( from - to ) / 2;
            
            const d = `M 0 ${50 - from * 50} Q 25 ${50 - from * 50} 50 ${50 - ( ( to + difference ) * 50 )} T 100 ${50 - to * 50} V 100 ${50 + to * 50} Q 75 ${50 + to * 50} 50 ${50 + ( ( to + difference ) * 50 )} T 0 ${50 + from * 50} V ${50 - from * 50} Z`;
            const svg = '<svg id="fcm-' + i + '-' + j + '" viewBox="0 0 100 100" preserveAspectRatio="none" style="width: 100%; height: 100%;"><path d="' + d + '" fill="black"></path></svg>';
            
            segments[j].style.setProperty( '--mask', ( 'url( #fcm-' + i + '-' + j + ' )' ) );
            segments[j].innerHTML = svg;
        }
    }
} );
.funnel > div:last-child {
    display: flex;
    flex-direction: row;
    height: 300px;
}

.funnel > div:last-child div {
    background: green;
    height: 100%;
    position: relative;
    width: 100%;
}

.funnel:not( .no-mask ) > div:last-child div {
    mask-image: var( --mask );
    mask-size: cover;
    mask-repeat: no-repeat;
}
<div class="funnel">
    <div>
        Funnel (Mask)
    </div>
    
    <div>
        <div data-from="1" data-to="0.66667"></div>
        <div data-from="0.66667" data-to="0.25"></div>
        <div data-from="0.25" data-to="0.1"></div>
    </div>
</div>


<div class="funnel no-mask">
    <div>
        Funnel (No Mask)
    </div>
    
    <div>
        <div data-from="1" data-to="0.66667"></div>
        <div data-from="0.66667" data-to="0.25"></div>
        <div data-from="0.25" data-to="0.1"></div>
    </div>
</div>

For each segment in the funnel, a script will draw the relevant SVG path for this segment, and add it (with a generated ID) into the DIV. The new ID will be passed to the DIV as a CSS variable (an attribute like style="--mask: url( #fcm-0-0 );").

The issue I’m facing, is that when I try using the black area of the SVG to mask the parent DIV, it seems not working and all colors are disappearing.

My goal is to mask the green background of my DIV with the SVG shape for now, so later I can replace the background with a gradient.

I’m adding in my snippet 2x examples:

  • The first one, is my issued code.
  • The second one is an example of my graph result (without masking) so you can see the SVG funnel segments.

Can you help with this please?

2

Answers


  1. I would define the SVG inside the mask variable instead of creating an SVG element then reference it.

    document.addEventListener('DOMContentLoaded', function() {
      let funnels = document.getElementsByClassName('funnel');
    
      for (let i = 0; i < funnels.length; i++) {
        let segments = funnels[i].children[1].children;
    
        for (let j = 0; j < segments.length; j++) {
          const from = parseFloat(segments[j].getAttribute('data-from'));
          const to = parseFloat(segments[j].getAttribute('data-to'));
          const difference = (from - to) / 2;
    
          const d = `M 0 ${50 - from * 50} Q 25 ${50 - from * 50} 50 ${50 - ( ( to + difference ) * 50 )} T 100 ${50 - to * 50} V 100 ${50 + to * 50} Q 75 ${50 + to * 50} 50 ${50 + ( ( to + difference ) * 50 )} T 0 ${50 + from * 50} V ${50 - from * 50} Z`;
          segments[j].style.setProperty('--mask', 'url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="none" style="width: 100%; height: 100%;"><path d="' + d + '" fill="black"></path></svg>')');
        }
      }
    });
    .funnel>div:last-child {
      display: flex;
      height: 300px;
    }
    
    .funnel>div:last-child div {
      background: green;
      flex: 1;
    }
    
    .funnel:not( .no-mask)>div:last-child div {
      mask-image: var( --mask);
    }
    <div class="funnel">
      <div>
        Funnel (Mask)
      </div>
    
      <div>
        <div data-from="1" data-to="0.66667"></div>
        <div data-from="0.66667" data-to="0.25"></div>
        <div data-from="0.25" data-to="0.1"></div>
      </div>
    </div>
    Login or Signup to reply.
  2. TL:DR: Converting the mask to a data URL as suggested by Temani Afif is probably the easiest workaround.

    You current code can’t work because your mask is not valid:
    When referencing inlined SVG masks (appended to your HTML DOM) you need to wrap the mask shape in a <mask> element – currently you’re referencing the whole SVG element – won’t work.

    There is another catch: mask-size property is ignored for inlined mask elements. See also "How to properly scale an SVG mask within a container element?"

    document.addEventListener( 'DOMContentLoaded', function() {
        let funnels = document.getElementsByClassName( 'funnel' );
        
        for( let i = 0; i < funnels.length; i++ ) {
            let segments = funnels[i].children[1].children;
            
            for( let j = 0; j < segments.length; j++ ) {
                const from = +segments[j].dataset.from;
                const to = +segments[j].dataset.to;
                const difference = ( from - to ) / 2;
                
                const d = `M 0 ${50 - from * 50} Q 25 ${50 - from * 50} 50 ${50 - ( ( to + difference ) * 50 )} T 100 ${50 - to * 50} V 100 ${50 + to * 50} Q 75 ${50 + to * 50} 50 ${50 + ( ( to + difference ) * 50 )} T 0 ${50 + from * 50} V ${50 - from * 50} Z`;
                const svg = 
                `<svg  viewBox="0 0 100 100" preserveAspectRatio="none" style="width: 100%; height: 100%;">
                  <mask id="fcm-${i}-${j}">
                    <rect width="100%" height="100%" fill="#000" />
                    <path d="${d}" fill="#fff"  />
                  </mask>
                </svg>`;
                
               segments[j].style.setProperty( '--mask', ( 'url( #fcm-' + i + '-' + j + ' )' ) );
               segments[j].innerHTML = svg;
            }
        }
    } );
    body{
      background:#ccc;
    }
    .funnel{
      background:#999;
    }
    
    
    .funnel > div:last-child {
        display: flex;
        flex-direction: row;
        height: 300px;
    }
    
    .funnel > div:last-child div {
        background: green;
        height: 100%;
        position: relative;
        width: 100%;
    }
    
    .funnel:not( .no-mask ) > div:last-child div {
        -webkit-mask-image: var( --mask );
        mask-image: var( --mask );
        mask-size: 100%;
        mask-repeat: no-repeat;
    }
    <div class="funnel">
        <div>
            Funnel (Mask)
        </div>
        
        <div>
            <div data-from="1" data-to="0.66667"></div>
            <div data-from="0.66667" data-to="0.25"></div>
            <div data-from="0.25" data-to="0.1"></div>
        </div>
    </div>
    
    
    <div class="funnel no-mask">
        <div>
            Funnel (No Mask)
        </div>
        
        <div>
            <div data-from="1" data-to="0.66667"></div>
            <div data-from="0.66667" data-to="0.25"></div>
            <div data-from="0.25" data-to="0.1"></div>
        </div>
    </div>
    
    <!--
    <svg viewBox="0 0 100 100" preserveAspectRatio="none" style="width: 100%; height: 100%;">
    <mask id="mask">
          <rect width="100%" height="100%" fill="white" />
          <path d="M 0 0 Q 25 0 50 8.333250000000007 T 100 16.6665 V 100 83.3335 Q 75 83.3335 50 91.66675 T 0 100 V 0 Z" fill="black"/>
    </mask>
    </svg>
    -->

    As an alternative you may also opt for a responsive <clipPath>

    document.addEventListener('DOMContentLoaded', function() {
      let funnels = document.getElementsByClassName('funnel');
    
      for (let i = 0; i < funnels.length; i++) {
        let segments = funnels[i].children[1].children;
    
        for (let j = 0; j < segments.length; j++) {
          const from = +segments[j].dataset.from;
          const to = +segments[j].dataset.to;
          const difference = (from - to) / 2;
    
          const d = `M 0 ${50 - from * 50} Q 25 ${50 - from * 50} 50 ${50 - ( ( to + difference ) * 50 )} T 100 ${50 - to * 50} V 100 ${50 + to * 50} Q 75 ${50 + to * 50} 50 ${50 + ( ( to + difference ) * 50 )} T 0 ${50 + from * 50} V ${50 - from * 50} Z`;
          const svg =
            `<svg  viewBox="0 0 1 1" preserveAspectRatio="none" style="width: 0%; height: 0%; position:absolute;">
                  <clipPath id="fcm-${i}-${j}"  clipPathUnits="objectBoundingBox">
                    <path transform="scale(0.01)" d="${d}" fill="#fff"  />
                  </clipPath>
                </svg>`;
    
          segments[j].style.setProperty('--clip', (`url( #fcm-${i}-${j})`));
          segments[j].innerHTML = svg;
        }
      }
    });
    body {
      background: #ccc;
    }
    
    .funnel {
      background: #999;
    }
    
    .funnel>div:last-child {
      display: flex;
      flex-direction: row;
      height: 300px;
    }
    
    .funnel>div:last-child div {
      background: green;
      height: 100%;
      position: relative;
      width: 100%;
    }
    
    .funnel:not( .no-mask)>div:last-child div {
      clip-path: var(--clip);
      /* fix to avoid tiny gaps */
      margin-left: -0.5px;
    }
    <div class="funnel">
      <div>
        Funnel (Mask)
      </div>
    
      <div>
        <div data-from="1" data-to="0.66667"></div>
        <div data-from="0.66667" data-to="0.25"></div>
        <div data-from="0.25" data-to="0.1"></div>
      </div>
    </div>
    
    
    <div class="funnel no-mask">
      <div>
        Funnel (No Mask)
      </div>
    
      <div>
        <div data-from="1" data-to="0.66667"></div>
        <div data-from="0.66667" data-to="0.25"></div>
        <div data-from="0.25" data-to="0.1"></div>
      </div>
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search