Table of Contents
- Shaping Containers with CSS
- Two different solutions
- Why not just use a background image?
- The clip-path property can be used to shape the div.
- clip-path: path() doesn't scale well
- clip-path: url() is a step up from path()
- Images are block level elements and take up space in the document.
- Making the overlap magic happen
- Conclusion
Shaping Containers with CSS
HTML elements on a webpage are rectangular by default but can take on both simple and complex shapes with the use of clip-path
. However, making complex shapes with clip-path
requires using an SVG path or file. If you want to use another file type like a transparent PNG, you wouldn't be able to use clip-path
.
That being said SVG is preferable if available as it typically comes with a smaller file size and should scale to any size needed.
Almost any section can be modified to take on a non-rectangular shape but I tend to use them either in the header or footer of a webpage as this often pairs with the main branding of the website.
Two different solutions
There are a few different methods that will work depending on your needs. clip-path
paired with an SVG will be the ideal method. An alternate method will be shown when using a transparent PNG.
Why not just use a background image?
This is a good question as background images are easy to place and size but the biggest issue is that they don't take up their own space in the document. In fact, background images are contained and clipped by their own element. That means you can't use a background image to overlap anything outside of it's containing element.
clip-path
property can be used to shape the div.
The Using clip-path
on an element with content or an element with a explicit height is useful for simple shapes. For example, if you wanted to give a section division a slant, you can easily do this using an empty div with an explicit height with clip-path: polygon()
.
clip-path: path()
doesn't scale well
The path()
value for the clip-path
property looks really promising. You can just pop a path string into the function and get an SVG path clipped to the element. Unfortunately, and I'm not sure why, the numbers representing the path are converted to pixels which removes any real possibility of making this responsive.
clip-path: url()
is a step up from path()
url()
allows you to reference an SVG with a <clipPath>
by it's ID. Thankfully, it doesn't suffer from the same scaling problem as path as long as you specify the clipPathUnits
value as objectBoundingBox
.
Images are block level elements and take up space in the document.
Because images are block level elements, they take up space on the screen. Add in some tweaks to the element's position and you can easily overlap the image with the element above it.
Making the overlap magic happen
In my use case, the transparent top of the image need to overlap the last child of the previous sibling (the main tag) to allow the background from the last child to flow into the footer transparency.
To do this, I started by changing the footer's position to absolute and placing it at the bottom with bottom: 0
. To offset the footer from the last child of the main tag, bottom padidng was added to the last child.
In my project, I couldn't know in advance which element would be before the footer but I did know it would be the last element in the main element of the body of the document. To fix this, I used a little JavaScript to dynamically get the last child of the main element and set it's bottom padding equal to the height of the footer.
// this sets the last child's padding bottom to the height of the footer which prevents the footer from overlapping the last child's content.
const footer = document.getElementsByTagName("footer")[0];
const main = document.getElementsByTagName("main")[0];
const lastChild = Array.from(main.children)[main.childElementCount - 1];
// give enough bottom padding to keep footer below block
lastChild.style.setProperty(
"padding-bottom",
`${Math.ceil(footer.getBoundingClientRect().height)}px`,
"important"
);
window.addEventListener("resize", () => {
lastChild.style.setProperty(
"padding-bottom",
`${Math.ceil(footer.getBoundingClientRect().height)}px`,
"important"
);
});
A few things to note here:
- this assumes the previous sibling is the last child of the main element
- Math.ceil is used to ensure the value is an integer but isn't strictly needed
- I'm using set property passing "important" to override a Bootstrap class with an "important" value
- I'm also using a resize listener in case the browser window size changes but isn't necessary to make this script work
A stripped down version:
const footer = document.getElementsByTagName("footer")[0];
const main = document.getElementsByTagName("main")[0];
const lastChild = Array.from(main.children)[main.childElementCount - 1];
lastChild.style.paddingBottom = `${Math.ceil(
footer.getBoundingClientRect().height
)}px`;
If you weren't sure what the last child of the previous sibling is, you could make this more dynamic by replacing the line with const main
with footer.previousElementSibling.children[footer.previousElementSibling.childElementCount -1]
which would get the last child of the previous sibling without knowing what is was beforehand.
Conclusion
So there we have it, a few different way to achieve the same eye-catching effect depending on the situation.