pjk_ok
pjk_ok

Reputation: 967

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>

Upvotes: 37

Views: 53750

Answers (4)

Temani Afif
Temani Afif

Reputation: 274297

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>

Upvotes: 35

Paulo Amorim
Paulo Amorim

Reputation: 41

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" />

Upvotes: 3

leonheess
leonheess

Reputation: 21520

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.

Upvotes: 21

Josh Mc
Josh Mc

Reputation: 10254

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>

Upvotes: 9

Related Questions