Reputation: 967
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
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/
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
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
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
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