Search code examples
htmlcsssvgclip-path

Create a Reverse Clip-Path - CSS or SVG


I'm trying to create what is in essence the reverse of a CSS clip-path. When using clip-path, an image or div is clipped so that only the shape you specify remains and the rest of the background is effectively deleted.

I would like it so that if I clip a shape it basically punches a hole in the upper most layer and removes the shape, not the background. Is this possible? I'd also be open to an SVG solution, but I am new to SVG so be kind :)

Basically, in the code below I have a blue square positioned absolutely inside a red square and want to be able to punch a shape out of the blue square so the red layer below shows through where the shape used to be. In reality there will an image as the background layer, so I can't accept a pseudo effect that mimics what I want but doesn't actually punch the shape out.

Any assistance would be amazing!

codepen: https://codepen.io/emilychews/pen/GQmyqx

body {
  width: 100%; 
  height: 100vh;
  padding: 0; margin: 0;
  display: flex;
  }

#box {
  margin: auto;
  position: relative;
  width: 33%;
  height: 200px;
  background: red;
}

#innerbox {
  width: 100%;
  height: 100%;
  background: blue;
  top: 0;
  left: 0;
  position: absolute;
}
<div id="box">
  <div id="innerbox"></div>
</div>


Solution

  • Update 2025

    I shared a trick on my blog where you can easily invert any clip-path: polygon(): https://css-tip.com/cut-out-shapes/

    Inverted clip-path

    Examples:

    .shape {
      --shape: 0 0,100% 0,50% 100%,0 0; /* the first value is repeated at the end */
      
      height: 120px;
      aspect-ratio: 1;
      clip-path: polygon(var(--shape));
      background: linear-gradient(-45deg,#CD8C52,#5E9FA3);
    }
    .shape.invert {
      --s: -20px; /* to control the space */
      padding: calc(-1*var(--s));
      box-sizing: content-box; 
      clip-path: 
        polygon(evenodd,var(--s) var(--s),calc(100% - var(--s)) var(--s),calc(100% - var(--s)) calc(100% - var(--s)),var(--s) calc(100% - var(--s)),var(--s) var(--s),var(--shape)) content-box; 
    }
    
    /* defining the shapes */
    .starburst {
      --shape: 100% 50%,78.98% 57.76%,93.3% 75%,71.21% 71.21%,75% 93.3%,57.76% 78.98%,50% 100%,42.24% 78.98%,25% 93.3%,28.79% 71.21%,6.7% 75%,21.02% 57.76%,0% 50%,21.02% 42.24%,6.7% 25%,28.79% 28.79%,25% 6.7%,42.24% 21.02%,50% 0%,57.76% 21.02%,75% 6.7%,71.21% 28.79%,93.3% 25%,78.98% 42.24%,100% 50%;
    }
    .chevron {
      --c: 40%;
      --shape: 0 0,var(--c) 0,100% 50%,var(--c) 100%,0 100%,calc(100% - var(--c)) 50%,0 0;
      aspect-ratio: 3/5;
    }
    .triangle {
      --shape: 100% 0,0 50%,100% 100%,100% 0;
      aspect-ratio: 1/2;
    }
    .pentagon {
      --shape: 79.39% 90.45%,20.61% 90.45%,2.45% 34.55%,50% 0%,97.55% 34.55%,79.39% 90.45%;
    }
    
    body {
      display: grid;
      grid-auto-flow: column;
      grid-template-rows: auto auto;
      place-items: center;
      gap: 10px;
    }
    <div class="shape starburst"></div>
    <div class="shape starburst invert"></div>
    
    <div class="shape chevron"></div>
    <div class="shape chevron invert"></div>
    
    <div class="shape triangle"></div>
    <div class="shape triangle invert"></div>
    
    <div class="shape pentagon"></div>
    <div class="shape pentagon invert"></div>


    Old answer

    You can put the image above the blue part and you apply the clip-path on it then the result will be the same as if you have created a hole inside the blue part to see the image below:

    body {
      width: 100%; 
      height: 100vh;
      padding: 0; margin: 0;
      display: flex;
      }
    
    #box {
      margin: auto;
      position: relative;
      width: 33%;
      height: 200px;
      background: blue;
    }
    
    #innerbox {
      background: url(https://picsum.photos/400/400/) center/cover;
      position: absolute;
      inset: 0;
      z-index:1;
      clip-path:polygon(10% 10%, 10% 90%, 90% 50%);
    }
    <div id="box">
      <div id="innerbox"></div>
    </div>

    Another idea is to consider multiple background and you will have better support than clip-path and also less of code:

    body {
      height: 100vh;
      margin: 0;
      display: flex;
    }
    
    #box {
      margin: auto;
      position: relative;
      width: 33%;
      height: 200px;
      background: 
        linear-gradient(to bottom right,#0000 49%,blue 50%) bottom/100% 60%,
        linear-gradient(to top right,#0000 49%,blue 50%) top/100% 60%,
        linear-gradient(blue,blue) left/20% 100%,
        url(https://picsum.photos/400/400/) center/cover;
      background-repeat:no-repeat;
    }
    <div id="box">
    </div>

    UPDATE

    If you want some opacity, here is an idea where you have to duplicate the content using clip-path (a drawback):

    body {
      width: 100%; 
      height: 100vh;
      padding: 0; margin: 0;
      display: flex;
      }
    
    #box {
      margin: auto;
      position: relative;
      width: 33%;
      height: 200px;
      background: blue;
    }
    
    #innerbox,#innerbox-2 {
      background: url(https://picsum.photos/400/400/) center/cover;
      position: absolute;
      inset: 0;
      z-index:2;
    }
    #innerbox {
      /* if you initially planned to have x opacity so you need to set 1-x here*/
      opacity:0.4;
    }
    
    #innerbox-2 {
      z-index:1;
      clip-path:polygon(10% 10%, 10% 90%, 90% 50%);
      animation:animate 5s linear alternate infinite;
    }
    
    @keyframes animate {
      from {
        clip-path:polygon(10% 10%, 10% 90%, 90% 50%);
      }
      to {
         clip-path:polygon(20% 50%, 90% 50%, 80% 10%);
      }
    }
    <div id="box">
      <div id="innerbox">
        <h1>Title</h1>
        <p>Some content</p>
      </div>
      <div id="innerbox-2">
        <h1>Title</h1>
        <p>Some content</p>
      </div>
    </div>

    UPDATE 2

    You can consider SVG to do your initial requirement. Simply use an SVG instead of a div where you will have a mask.

    body {
      width: 100%; 
      height: 100vh;
      padding: 0; margin: 0;
      display: flex;
      }
    
    #box {
      margin: auto;
      position: relative;
      width: 33%;
      height: 200px;
      background: blue;
      background: url(https://picsum.photos/400/400/) center/cover;
    }
    
    #innerbox {
      position: absolute;
      inset: 0;
      z-index:1;
    }
    <div id="box">
      <svg viewBox="0 0 200 200" id="innerbox" preserveAspectRatio="none">
      <defs>
        <mask id="hole">
          <rect width="100%" height="100%" fill="white"/>
          <!-- the hole defined a polygon -->
          <polygon points="20,20 20,180 180,100 " fill="black"/>
        </mask>
      </defs>
      <!-- create a rect, fill it with the color and apply the above mask -->
      <rect fill="blue" width="100%" height="100%" mask="url(#hole)" />
    </svg>
    </div>

    You can also use the same SVG as background:

    body {
      width: 100%; 
      height: 100vh;
      padding: 0; margin: 0;
      display: flex;
      }
    
    #box {
      margin: auto;
      position: relative;
      width: 33%;
      height: 200px;
      background: blue;
      background: url(https://picsum.photos/400/400/) center/cover;
    }
    
    #innerbox {
      position: absolute;
      inset: 0;
      z-index:1;
      background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" preserveAspectRatio="none"><defs><mask id="hole"><rect width="100%" height="100%" fill="white"/> <polygon points="20,20 20,180 180,100 " fill="black"/></mask></defs><rect fill="blue" width="100%" height="100%" mask="url(%23hole)" /></svg>');
    }
    <div id="box">
      <div id="innerbox"></div>
    </div>

    Update 3 (what I recommend in 2020)

    You can use CSS mask to get the effect you want with mask-composite

    body {
      width: 100%; 
      height: 100vh;
      padding: 0; margin: 0;
      display: flex;
      }
    
    #box {
      margin: auto;
      position: relative;
      width: 33%;
      height: 200px;
      background: url(https://picsum.photos/400/400/) center/cover;
    }
    
    #innerbox {
      position: absolute;
      inset: 0;
      -webkit-mask:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" preserveAspectRatio="none"><polygon points="20,20 20,180 180,100 " fill="black"/></svg>') 0/100% 100%;
              mask:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" preserveAspectRatio="none"><polygon points="20,20 20,180 180,100 " fill="black"/></svg>') 0/100% 100%;
      background: blue;
    }
    <div id="box">
      <div id="innerbox"></div>
    </div>

    And the inverted version using the same shape

    body {
      width: 100%; 
      height: 100vh;
      padding: 0; margin: 0;
      display: flex;
      }
    
    #box {
      margin: auto;
      position: relative;
      width: 33%;
      height: 200px;
      background: url(https://picsum.photos/400/400/) center/cover;
    }
    
    #innerbox {
      position: absolute;
      inset: 0;
      -webkit-mask:
         url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" preserveAspectRatio="none"><polygon points="20,20 20,180 180,100 " fill="black"/></svg>') 0/100% 100%,
         linear-gradient(#fff,#fff);
      -webkit-mask-composite:destination-out;
              mask:
         url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" preserveAspectRatio="none"><polygon points="20,20 20,180 180,100 " fill="black"/></svg>') 0/100% 100%,
         linear-gradient(#fff,#fff);
      mask-composite:exclude;  
      background:blue;
    }
    <div id="box">
      <div id="innerbox"></div>
    </div>