Need to lay out an element to the right or the left, such that text wraps around it? That’s an easy task for the float
property. But what about if you also want to push that element (let’s call it an image) to one of the bottom corners while we’re at it? Sounds a bit tricky, right? We probably need JavaScript?
Nope, few lines of (tricky) CSS can do it! Here’s the CSS-only solution that will make your image to stick to the bottom corner, regardless of the size and content.
Resize the wrapper element and see the magic at work:
Let’s dissect the code.
Markup and layout
We’ll need a wrapper element to contain everything, and we’ll be using flexbox on it. Flexbox allows us to rely on the default stretch
alignment to be able to later use height: 100%
.
<div class="wrapper"> <div class="box"> <div class="float"><img></div> Lorem ipsum dolor ... </div>
</div>
.wrapper { display: flex;
} .float { float: right; height: 100%; display: flex; align-items: flex-end; shape-outside: inset(calc(100% - 100px) 0 0);
}
The .box
within the .wrapper
is our flex item. We don’t need any particular CSS applied to the box. It defines the height of the wrapper and, at the same time, is stretched to the same height. This behavior will give us a “reference height” that can be used by the child elements.
From the specification:
If the flex item has
align-self: stretch
, redo layout for its contents, treating this used size as its definite cross size so that percentage-sized children can be resolved.
The keyword is the definite which allows us to safely use a percentage (%) height inside the box element.
Now for the floated element
Our .float
element will take the entire height next to the text content, thanks to the height calculation we detailed above. Inside this element we push the image to the bottom using flexbox alignment.
Now for the real trickery, using the shape-outside property. Here’s how MDN defines it:
The shape-outside CSS property defines a shape—which may be non-rectangular—around which adjacent inline content should wrap. By default, inline content wraps around its margin box;
shape-outside
provides a way to customize this wrapping, making it possible to wrap text around complex objects rather than simple boxes.
In other words, shape-outside
sets the way content flows around an element’s bounding box.
It takes a number of values. One of those is the inset()
function which, again, according to MDN:
Defines an inset rectangle. When all of the first four arguments are supplied they represent the top, right, bottom and left offsets from the reference box inward that define the positions of the edges of the inset rectangle.
So, with shape-outside: inset(calc(100% - X) 0 0)
we can create an inset rectangle that starts exactly at the top of the image. And the top is equal to 100% - X
, where X
is the image height and 100%
is the height of the .float
element. This allows the text to wrap within the free space on the top of the image. This is responsive, plus we can easily switch between left and right (by adjusting the float
property)
That’s it! The only major caveat is that you need to know the image height.
Want more?
We can extend this concept a little further to account for fancier situations. For example, we can float the image to the right, but pin it to the middle of the box with justify-content: center
: and also adjust our inset rectangle to the middle by changing the shape-outside
from inset(calc(100% - X) 0 0)
to inset(calc(50% - X/2) 0 0)
We can also float two images at both bottom corners:
Nothing complex here. I am simply using the same floating element twice, once on the right, and again on the left. And why stop at two corners when we can place images at all four corners:
The same basic idea is at play here, but we’re are also relying on the common float feature for the top images. However, you’ll notice that this is where the concept starts to break down a bit, and we get some unwanted overflow depending on the size of the containing box. We can make the height of the .float
element greater than 100% and apply somewhat “magic numbers” that smooth things out by adjusting the padding and margin of the images.
Did you know that shape-outside
accepts radial-gradient()
as a value? We can use that to place rounded images like below:
The transparent part of the gradient is the free space where the text can go. You may have noticed that we applied a border-radius
to the image as well. The shape-outside
property will simply affect the .float
element and we need to manually adjust the shape of the image to follow the shape defined by shape-outside
.
While we’re at it, let’s combine this with our earlier example that pins the image to the vertical center of the box using justify-content: center
:
Another radial-gradient()
and also another border-radius
configuration.
We could have used a linear-gradient()
instead to make a triangular shape for the wrapping area:
This is the same idea that we used for the radial-gradient()
. The big difference is that we’re using clip-path
instead of border-radius
to cut our image.
And, since we did it for the others, let’s use the justify-content: center
idea to pin the image to the vertical center of the box’s right edge:
We used a conic-gradient()
in the above demo with shape-outside
to define the triangular shape and clip-path
to get a similar shape on the image
All of these examples can still be optimized using less of code in the case that the image is decorative (when it’s not needed inside the HTML for SEO purposes). Let’s replace the .float
element with a pseudo-element and apply the image as background instead:
We’re using mask
to show just the portion of the image that we need and, guess what, it uses the same value as shape-outside
! So, all we had to do is define one value for the shape.
That’s it!
There are a lot of possibilities here to place not just rectangles in corners, but any kind of shape at any position, using largely the same code structure. We only need to:
- Adjust the
shape-outside
property to define the shape - Apply some styling to the image to follow the previously defined shape or apply the same value to
mask
in case we are using the pseudo element version
Then everything holds it place, even in responsive designs.