30

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>
pjk_ok
  • 618
  • 7
  • 35
  • 90

4 Answers4

30

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>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • That is very clever. But I do need to actually punch a hole in the blue layer, because the background will either be an image or video, but there will also be text. What i need is the effect you've just done, but with the blue layer to opacity .9 and have the image or video fill the entire container, but be 100% clear where the shape is. I'm not sure it's going to be possible. – pjk_ok Feb 11 '18 at 23:15
  • @TheChewy ok so let's say you want opacity with clip-path :) – Temani Afif Feb 11 '18 at 23:35
  • @TheChewy check again, i added another idea :) – Temani Afif Feb 11 '18 at 23:41
  • Yes that will work with an image. I'm not sure I could get away with that if the client goes down the video route (which they may well do), because it would mean loading two large video elements, which would mean a pretty big performance hit. I'm going to post a separate question using an SVG overlay i've done with a hole punched out of it, and see if it's possible to responsively align it. I do appreciate your help and will defo consider the above for an image based solution. – pjk_ok Feb 12 '18 at 00:00
  • @TheChewy ok, will be on that question too :) – Temani Afif Feb 12 '18 at 00:02
18

This ranks high on Google and the answer didn't solve my problem b/c I cannot touch my background image so here is another way of doing this:

Create a frame with the clip-path.

body {
  width: 100%;
  height: 100vh;
  padding: 0;
  margin: 0;
  display: grid;
  place-items: center;
}

#clip,
#background {
  width: 400px;
  height: 400px;
}

#clip {
  clip-path: polygon(0% 0%, 0% 100%, 25% 100%, 25% 25%, 75% 25%, 75% 75%, 25% 75%, 25% 100%, 100% 100%, 100% 0%);
  position: absolute;
  background: #fff;
  opacity: 0.8;
}

#background {
  background: url(https://picsum.photos/400/400/) center/cover;
  z-index: -1;
}
<div id="background">
  <div id="clip"></div>
</div>

I put the clip-div inside the image because of convenience but you can also have it outside.

leonheess
  • 16,068
  • 14
  • 77
  • 112
7

To expand upon @leonheess great work with the aid of var() and calc(), you can setup variables for x/y/width/height and easily move around your square based on js familiar properties.

#clip-container {
  --windowposition-x: 50px;
  --windowposition-y: 50px;
  --windowposition-height: 100px;
  --windowposition-width: 100px;
}

body {
  width: 100%;
  height: 100vh;
  padding: 0;
  margin: 0;
  display: grid;
  place-items: center;
  background: url(https://picsum.photos/400/400/) center/cover;

}

#clip-container {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(197, 185, 185, 0.7);
  clip-path: polygon(0% 0%,
        0% 100%,
        var(--windowposition-x) 100%,
        var(--windowposition-x) var(--windowposition-y),
        calc(var(--windowposition-x) + var(--windowposition-width)) var(--windowposition-y),
        calc(var(--windowposition-x) + var(--windowposition-width)) calc(var(--windowposition-y) + var(--windowposition-height)),
        var(--windowposition-x) calc(var(--windowposition-y) + var(--windowposition-height)),
        var(--windowposition-x) 100%,
        100% 100%,
        100% 0%);
}
  <div id="clip-container"></div>

If you really wanted you could even take this a step further and define your css vars in your html like:

body {
  width: 100%;
  height: 100vh;
  padding: 0;
  margin: 0;
  display: grid;
  place-items: center;
  background: url(https://picsum.photos/400/400/) center/cover;

}

#clip-container {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(197, 185, 185, 0.7);
  clip-path: polygon(0% 0%,
        0% 100%,
        var(--windowposition-x) 100%,
        var(--windowposition-x) var(--windowposition-y),
        calc(var(--windowposition-x) + var(--windowposition-width)) var(--windowposition-y),
        calc(var(--windowposition-x) + var(--windowposition-width)) calc(var(--windowposition-y) + var(--windowposition-height)),
        var(--windowposition-x) calc(var(--windowposition-y) + var(--windowposition-height)),
        var(--windowposition-x) 100%,
        100% 100%,
        100% 0%);
}
  <div id="clip-container" style="--windowposition-x: 75px;--windowposition-y: 75px;--windowposition-height: 75px;--windowposition-width: 75px;"></div>
Josh Mc
  • 9,911
  • 8
  • 53
  • 66
  • Any idea how to do this for a polygon with n number of points as the "hole"? – Nic Estrada Jul 06 '23 at 01:00
  • @NicEstrada - I don't think a dynamic number of vertices could be easily achieved using the method above, at least its not in the realms of my understanding of css vars sorry. – Josh Mc Jul 07 '23 at 01:59
  • I actually got it working, (only if the assumption could be made that the polygon's points were listed in clockwise order) but then discovered that my requirements included needing multiple polygon cutouts and I actually needed to use a mask. – Nic Estrada Jul 07 '23 at 15:52
  • Nice - feel free to answer here, might help someone else :D – Josh Mc Jul 08 '23 at 11:37
0

Since this didn't work for me and I did some testing and just solved my issue, let me share it:

You can just use a clip-path with an SVG path making it cover all the area you want to appear and then another path inside of it, in the opposite direction, to create the hole (hard to explain, easy to do...).

You may need to generate this path on code based on the size of your element, but it's easily done:

const element = document.getElementById('element')

const width = element.clientWidth
const height = element.clientHeight
const holeX = 50
const holeY = 30
const holeSize = 60

const holePath = `M ${holeX} ${holeY} L ${holeX + holeSize} ${holeY} L ${holeX + holeSize} ${holeY + holeSize} L ${holeX} ${holeY + holeSize} L ${holeX} ${holeY}`

const path = `M 0 0 L 0 ${height} L ${width} ${height} L ${width} 0 L 0 0 ${holePath} Z`

element.style.clipPath = `path('${path}')`
body {  
  background-color: #09f;
}

#element {
  background: url(https://picsum.photos/500/250/);
  width: 500px;
  height: 250px;
}
<div id="element" />

In case you want to do a different hole format, just grab some SVG path and throw it in there in the holePath. It should work.

const element = document.getElementById('element')

const width = element.clientWidth
const height = element.clientHeight
const holeX = 50
const holeY = 150
const holeSize = 60

const holePath = `M ${holeX} ${holeY} v-100 h100 a50,50 90 0,1 0,100 a50,50 90 0,1 -100,0`

const path = `M 0 0 L 0 ${height} L ${width} ${height} L ${width} 0 L 0 0 ${holePath} Z`

element.style.clipPath = `path('${path}')`
body {  
  background-color: #09f;
}

#element {
  background: url(https://picsum.photos/500/250/);
  width: 500px;
  height: 250px;
}
<div id="element" />