Yet another take on layout breakouts
I have now implemented a much more powerful and flexible version of this approach as Mathy Margins. Check it out!
A common design pattern is content that fills the screen with some gutter on small viewports, then has some maximum width to keep things readable. Sometimes, though, you want something that’s wider than that maximum width, either full screen width (edge-to-edge), or that breaks out, where it’s wider by some amount when there’s enough space.
How to implement these “layout breakouts” is very well worn territory, but here’s my take. Is it novel in 2024? I have no idea.
:where(.max-inline-size-padded) {
& > * {
/* default configuration */
--max-inline-size: 50rem;
--gutter: 16px;
max-inline-size: var(--max-inline-size);
margin-inline: max((100% - var(--max-inline-size)) / 2, var(--gutter));
}
}
Walking through this, it starts by setting up a utility class. I called it max-inline-size-padded because I’m a big fan of logical properties; inline-size is width in horizontal languages like English.
Then, using native CSS nesting, it sets some properties on & > *, which means all direct children of an element with the max-inline-size-padded class. The first couple of declarations set custom properties for the default configuration. --max-inline-size is how wide the content is allowed to get, and --gutter is how much space to leave on the left and right (aka inline-start and inline-end) when the container is smaller than the maximum width.[1]
The next two lines are the heart of this approach. Remember, these apply not to the container, but to its immediate children. The first declaration caps that inline size at the value we provided.
The second line centers the content by calculating the margin. The first argument to the max() function handles the case when the container is wide enough that the max size gets imposed. It takes the full width of the container and subtracts out the max width of the content, leaving the remaining available space. Then, it divides that in half because half of margin-inline is applied to the left (inline-start) and half is applied to the right (inline-end). This has the same effect as the typical way you’d center something with margin-inline: auto. The problem with that is that you can’t do math on auto[2]. Since we need to use max(), we need to do the calculation that comes up with the value that auto would normally give us for free.
The second argument to max() is the gutter. When the container is large enough that there’s more than --gutter remaining after subtracting/halving --max-inline-size out of 100% of the inline size, the first argument is larger so that’s the value of margin-inline. But when the container is smaller than that, the first argument is small (usually negative), so --gutter is greater and that gets applied. That ensures that there’s always at least --gutter of margin on each side.
So far so good, but we haven’t talked about breakouts yet. Here’s what I like about this technique: any child can override either or both of the configuration properties (--max-inline-size and --gutter). Items could do that as one-offs, but we can also provide additional utilities like these for common cases:
:where(.max-inline-size-padded) {
/* ✂️ */
& > .breakout {
--max-inline-size: 54rem;
}
& > .edge-to-edge {
--max-inline-size: 100%;
--gutter: 0px;
}
}
Here, any child of our .max-inline-size-padded container with a breakout class is allowed to get wider than regular content (54rem as opposed to 50rem), and any child with the edge-to-edge class goes, well, edge to edge.
Additionally, this technique works well when nested, so you can do things like make an edge-to-edge child element be a container for additional content at the same time.
<main class="max-inline-size-padded">
<!-- ✂️ -->
<div class="special-block edge-to-edge max-inline-size-padded">
<p>This is some text that's nested inside an edge-to-edge element and that is a `max-inline-size-padded` container itself. Lorem ipsum dolor, sit amet consectetur adipisicing elit. Tenetur recusandae a et ea! Cumque, magnam quod. Maiores illo esse tenetur eligendi nisi incidunt adipisci odio cupiditate possimus, laudantium obcaecati iste!</p>
<aside class="breakout">Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio illo, autem fugiat labore eius dolorem cupiditate commodi maiores consequatur aspernatur neque voluptas eligendi beatae praesentium ut, nostrum nemo, quod nesciunt?</aside>
</div>
</main>
This technique is similar to Solution 3(b) in Michelle Barker’s summary of layout breakout implementations. In her example, there’s a simple margin-inline: auto, which introduces a problem that she details: you can’t adjust an item’s size without having it centered.
This solution doesn’t have that issue. Setting max-inline-size (not --max-inline-size) on a content element will limit its size without changing the margin on both sides, so it stays justified inline start.
One way of thinking about it is that --max-inline-size adjusts the size of the margin-imposed “pseudocontainer”, not of the element itself.[3] Changing the custom prop will center the item because it does change the margin on both sides. On the other hand, changing max-inline-size will just change the max size of the item.
Another issue Michelle points out is that the specificity of the utility class can make it hard to override properties for individual items. I addressed that here with a simple :where(), but in a production environment using @layers may be a better option.
Overall, this approach is very flexible but isn’t without its shortcomings. You can set max-inline-size to keywords like fit-content, but you can’t do that with --max-inline-size because (as with auto) you can’t calculate with keywords. (As noted before, that will hopefully change at some point.)
Similarly, inline alignment (aka justification) will be easier eventually because you should be able to set max-inline-size to set the max size, --max-inline-size (or a renamed prop that does the same thing) to set the inset or outset/breakout, and use justify-self to align it start, center, or end. But browser support isn’t there yet for using justify-self in block layouts.
There are a bunch of examples of sizing and aligning items in the demo on CodePen. As always, feedback welcome!
See the Pen Layout breakouts (again) by Noah (@noleli) on CodePen.
I have now implemented a much more powerful and flexible version of this approach as Mathy Margins. Check it out!
“Padding” is a bit of a misleading word since this technique doesn’t actually use anyInspired by Andy Bell, I have renamed this property frompadding, but to me it’s a concise descriptor of what that property does. It could just as easily be called--min-inline-marginor something, but since this technique is usually done with padding on the container, that’s how I think of it.--padding-inlineto--gutter. ↩︎I just learned about the proposed
calc-size()function that will let you do calculations with keyword-based sizes likeautoandmax-content. If it works the way I understand it, that could replace this calculation in themax()function when available. ↩︎And I’m realizing that the
--max-inline-sizeproperty is perhaps also misleadingly named. ↩︎