Breaking Down Boxes with CSS Shapes
For my last (and first) CodePen blog post, I wrote about CSS Variables, a feature currently available only in Firefox. This time, I'm trying out a new feature first available in webkit (Chrome and Opera so far, Safari prefixed), so switch browsers if need be to see what the future of the web will look like.
Little Boxes, on a Web Page,
Little Boxes in a Row
A fundamental feature of CSS layout, and even of pre-CSS HTML layout, is the box model. All the content on the web page is in rectangular layout boxes. The browser window is a rectangular box. Divisions, headings, paragraphs, are rectangular boxes, stacked to fill the page layout. Inside the layout boxes are inline boxes, like rectangular strips of tape, neatly filling the space in between the padding in tidy rectangular rows.
Sure, boxes don't always have to look like rectangles. Images can have transparent areas. Borders can have rounded corners. Boxes can even be rotated, and clipped where they poke out of other boxes, to create things that look like triangles or other shapes.
But all those changes are pure decoration. As far as the layout of material on the page has been concerned, everything is still rectangles inside rectangles.
The CSS Shapes Module is the first step towards changing that, towards breaking out of the boxes and creating layouts that fit content, whatever the content is.
Getting In Shape
The Level 1 version of the specification became a W3C Candidate Recommendation (meaning that it is no longer considered experimental, and the syntax is stable) in March 2014. It is implemented in Chrome version 37+ and Opera version 24+, without any developer flags, and is supported with prefixes in the latest Safari. Adobe, which has been strongly advocating advanced web layout specifications, has created a polyfill JavaScript to mimic the results in any browser that supports CSS2.1 (which is basically any browser out there except plain text and screen readers).
So what does it do? Can we build crazy-quilt web pages with text going every which way to somehow fill up the page just right? Not quite.
CSS Shapes, Level 1, applies to floated elements. Floats already break out of the box model in the sense that they are not neatly inside one box or another. The inline content of their parent and sibling elements wraps around them, until a sibling has a clear
property or a parent has an overflow
property. So floats don't fit neatly in boxes. However, up until now, floats have always been boxes. Specifically, the area occupied by a float—the area which the inline content wraps around—always had to be a rectangle. And not just any rectangle, but the same rectangle, defined by the float's layers of padding, border and margin, that is used to position the float. With the new shapes properties, you can define how the inline content wraps around the floated element.
Three new CSS properties define how shapes and floats work: shape-outside
, shape-margin
, and shape-image-threshold
. Most of the magic happens in shape-outside
, the other properties just allow a little extra finesse.
shape-outside
defines the shape that should be used to wrap inline content around the float (it has no effect on non-floated elements). You can define it in three ways: using one of the existing boxes that surround the element, using a custom shape function, or using an image. The next pen shows real examples of all of the above (if you're using a browser that supports shapes):
Resize the outer frame to see how the text re-flows around the floats, or open the full pen in a separate window to follow along with the explanation.
Wrapping to an image
The floated lightbulb <img>
using a matching image in the CSS to define wrapping shape. If you use an image as the value of shape-outside
, the shape is extracted using the image's alpha (opacity) values. Transparent areas can have text wrapped through them. Notice, however, that the text only intrudes into the image's box from the side on which it would normally wrap; it doesn't use the transparent areas on the other side.
On its own, the shape-outside property would cause the text to bump up right to the edge of the opaque parts of the graphic. The shape-margin
property adds a buffer zone, stretching out from the "official" edge of the shape in all directions. And this is where the first complication arises: The shape used for the float will never be larger than the float's normal box margins. Since the opaque part of the clip art extends right to the edge of the image, we need to add a regular box margin in order for the shape margin to have its intended effect.
Here's the full CSS for the element:
img {
max-width: calc(100% - 10em);
min-width: 3em;
float: left;
shape-outside: url( http://upload.wikimedia.org/... );
shape-margin: 0.5em;
margin: 0.5em;
}
Wrapping to a box
The next way of defining the shape-outside
is to use a keyword referencing one of the layout boxes. But wait, you say, isn't the whole idea of CSS shapes to break out of layout boxes? It is, and it does. There are two differences between using a layout box to define a float's shape and using the default float layout. First, you get to decide which layout box to use: content-box
, padding-box
, border-box
, or margin-box
. Second, the shape defined by the layout box includes any rounded corners created with border-radius
.
The heading on the Shapes example pen consists of two floated <span>
s, each with shape-outside: padding-box
. If you hover the pen, the padding area will be filled in with a gray background so you can confirm that the text wraps to it. The two spans are spaced a few lines away from each other using margins on the floated elements. With normal floats, that extra space would be empty. With shape-outside
, the wrapped text fills in the gap.
The heading spans also use border radius to create a curved shape for the wrapped text. This works even though the spans don't have any borders, and the shape is defined as padding-box
. The border radius curves are extended inwards to the padding and outwards to the margins.
Here's the full CSS for the heading:
h1 {
margin: 0;
font-size: 250%;
}
h1 span {
float: right;
clear: right;
display: block;
text-align: right;
padding: 0.25em 0 0.25em 0.5em;
border-radius: 1em;
shape-outside: padding-box;
}
h1 span + span {
margin-top: 1.5em;
}
Wrapping to a custom shape
It's called the CSS Shapes Module, not the Wrapping-to-Images-and-Boxes Module, so it's about time we talked about shapes. Specifically, the four new CSS basic shapes functions: inset
(inset rectangle), circle
, ellipse
and polygon
. Each function defines a geometrical region, calculated relative to one of the layout boxes for an element. To use for the shape-outside
property, you provide one of the functions and optionally one of the layout box keywords given above; margin-box
is the default if a reference box isn't specified. The same functions are used in the clip-path
property of the CSS Masking specification.
For the spade-shaped SVG graphic in the example, I used a custom polygon function to approximate the curved right edge of the graphic. I didn't bother tracing out the left edge of the spade, since that would always be ignored when the SVG is floated to the left. When you hover the example page, the SVG is given a gray background fill, clipped to the same polygon. (Ignore the additional clipping effect on the actual shape, that seems to be a bug in how Chrome implements the clip-path.) The shape-margin
property is used again to add some spacing.
Full CSS, including the hover effect:
svg {
display: block;
float: left;
clear: left;
width: 10em;
margin-top: 3em;
shape-outside: polygon(50% 5%, 90% 75%, 90% 85%, 87% 92%, 80% 95%,
55% 95%, 55% 100%, 0% 100%, 0% 5%) content-box;
shape-margin: 1%;
}
body:hover svg {
background: gray;
-webkit-clip-path: polygon(50% 5%, 90% 75%, 90% 85%, 87% 92%, 80% 95%,
55% 95%, 55% 100%, 0% 100%, 0% 5%);
clip-path: polygon(50% 5%, 90% 75%, 90% 85%, 87% 92%, 80% 95%,
55% 95%, 55% 100%, 0% 100%, 0% 5%) content-box;
}
Wrapping to a gradient
The final float in the example uses a radial gradient to define the shape-outside
. The CSS Shapes specs don't mention anything about gradients, but as far as modern CSS is concerned, a gradient is just another type of image. Now, there really isn't any benefit to using an elliptical gradient function to create the image that defines the shape, instead of using the ellipse function directly. However, I wanted to use a gradient to demonstrate the shape-image-threshold
property.
If you hover the pen body, the colorful gradient background will be replace with a copy of the black-to-transparent gradient that defines the wrapping shape. You'll notice that the text overlaps grey areas of the gradient. Except, to be precise, they aren't grey areas. They are partially transparent black areas. The shape-image-threshold: 0.15
setting indicates that text should wrap over any areas of the shape-outside
image that are 15% opaque (alpha = 0.15) or less.
The relevant CSS:
figure {
display: block;
float: right;
clear: right;
width:calc(100% - 15em);
margin:0;
margin-left:calc(-100% + 25em);
height:20em;
border-radius:50% 0 0 50%;
background-image: radial-gradient(20em circle at right,
navy, lightcyan 40% , aliceblue);
shape-outside: radial-gradient(20em circle at right,
black, transparent 50% );
shape-image-threshold: 0.15;
}
Functional Details
I mentioned the four basic shape functions above, but didn't get into the specifics. Here are your options:
inset()
creates a rectangle or rounded rectangle. As the name implies, the shape is calculated as an inset from the reference layout box (themargin-box
by default). You need to tell it how far to inset your shape. Unlike the now-deprecated CSSrect()
function, the order of parameters has specifically been designed to be similar to other CSS properties: specify between one and four lengths or percentages, separated by whitespace, representing top, right, bottom, and left insets, with missing values filled in using the same rules as for the margin and padding shortcut properties.The inset rectangle does not inherit rounded corners from border radius. Instead, you can specify any corner radius you choose by adding the keyword
round
and then give corner radius parameters using the same syntax asborder-radius
.The final declaration could look something like
inset(1em 1.5em round 3em / 1em) border-box
.circle()
andellipse()
use syntax similar to radial gradients: you specify the shape radius (two values for ellipses) as a length, percentage, or keyword, and then use the keywordat
and give a position value as keywords or coordinates. All parameters are optional: the radius is by defaultclosest-side
and the position is by defaultcenter
. Positions and percentage radius values are calculated relative to the reference layout box. If you use a percentage radius for a circle, and the reference box isn't square, the distance is calculated relative to the geometrical average of the height and width.Final declarations could be as simple as
circle() border-box
or as complex asellipse(40% closest-side at left 30%) margin-box
.polygon()
defines an arbitrary straight-edged shape as a collection of vertices (corner-points) to be connected in order. Each vertex is specified as a whitespace-separated pair of lengths or percentages (calculated relative to the top-left corner of the reference box); subsequent coordinates must be separated by commas. You can usecalc
functions to define coordinates as a mix of relative and absolute distances.An optional parameter allows you to specify an SVG fill-rule keyword; the default
nonzero
is all you need forshape-outside
, since the fill rule only affects holes in the shape, which won't have an impact on the outer shape. If you do specify the keyword, it comes first and must be separated from the coordinates by a comma.A sample declaration for a right-floated element would be
polygon(100% 0, 0 0, 80px calc(50% - 3em), 80px calc(50% + 3em), 0 100%, 100% 100%) content-box;
. This shape starts at the top right corner (100% 0), draws a straight line to the top left (0 0) and then cuts an angled wedge into the content box, 80px deep, to a vertical edge 6em high and centered vertically, before angling back out to the bottom left corner and completing the shape by drawing back to the bottom right, which will be connected back to the top right. It's easier to just show you:
Bugs and Limitations
After you've read this far, now I tell you there are bugs and limitations? Sorry. Better to learn them now rather than when you are halfway through a project.
Hopefully the following bugs are just temporary hiccups in the current webkit implementation:
The layout engine sometimes breaks down, and text wraps into parts of the floats that it shouldn't. It happens in the example page at screen widths just wide enough for the heading floats to fit at the top of the page, adjacent to the lightbulb.
SVG files aren't supported as images for the
shape-outside
property (I had to use a PNG thumbnail version of the lightbulb).
Other problems and limitations stem from the specifications themselves:
There is no easy way to make the shape-outside match the visible content of the float. If you're using background images or clip paths, you have to repeat the image url or clip path shape in both properties. If you're using images in the markup, it's even more complicated. Example 7 in the specs uses
attr(src url)
to grab the image source from the element, but no browsers currently support the two-parameter version of theattr
function. Even if that was implemented, it wouldn't help if the image was using asrcset
to swap between different size image files—you'd end up downloading a high-quality image for display and a fallback image for the shape.Unlike the
clip-path
property, there is currently no way to use SVG viewBox coordinates in a shape function forshape-outside
.The shape-outside value only affects the way inline content wraps around the float. It doesn't affect the position of the float elements, or any content inside them. This can be used to good effect—it's how the floated headlines were separated from each other using margins—but you need to plan for it, and remember that the shape can never extend outside of the regular float margins.
The visual effect of the shape depends on lines of text wrapped around it. If the shape doesn't match up with an even number of lines, the space above and below will be irregular. If the area available for wrapped text is too short for a long word, that word and all subsequent lines of text will get pushed down until they can fit, leaving gaps. You can see both of these effects at certain screen widths in the
Shapely CSS Shapes
pen above.
The current specifications aren't the final word on CSS shapes, of course. There is already a related specification CSS Exclusions under discussion to allow wrapping around absolutely positioned elements, and plans for a CSS Shapes, Level 2 spec which would include a shape-inside
property to control the layout of text inside an element. Both bring up implementation difficulties which have not yet been resolved.
Polyfill or Poly-won't?
I mentioned at the start of the post that Adobe has a polyfill to emulate CSS shapes effects. You can read about it on their blog, check out their github repo, or grab their script from this pen and use it as an external resource for yours. It works by collapsing the margins on the actual floated elements, and replacing them by a series of 1em-high "sandbag" floated <div>
elements, which cumulatively build up the shape.
I tried applying it to my complex CSS shapes example, but the result was a bit of a disaster:
If you're on a browser that doesn't support CSS shapes, you'll see that the left and right floats are forced to clear each other, the polygon defined using percentage coordinates isn't used at all, and the z-index is thrown off. Read more about their known issues here. Hover the page to see the dynamically inserted elements. The original, no-polyfill version looks better without shapes.
For a more effective use of the polyfill, check out this example, taken from Adobe's CodePen CSS Shapes Gallery. I've forked their pen to add in the same hover effect, so you can see how the shape is built using empty floated divs:
If you're on a browser that supports resizeable elements you can stretch the frame to force a recalculation (it will wait until you stop dragging before snapping to the new layout). If you're on IE, Opera, or mobile, select "Edit" to open the pen in a new tab.
As you can see (when hovering the pen on a browser that needs the polyfill), there are a lot of extra elements added in. Plus, there's the script itself to download and run. In a complex web page, that could be an important performance hit. You'll have to decide for your own project whether the shapely layout is worth it, or whether you can gracefully degrade to normal float behaviour.