Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Fri, 31 Oct 2025 16:06:51 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Super Simple Full-Bleed & Breakout Styles https://frontendmasters.com/blog/super-simple-full-bleed-breakout-styles/ https://frontendmasters.com/blog/super-simple-full-bleed-breakout-styles/#comments Fri, 31 Oct 2025 16:06:50 +0000 https://frontendmasters.com/blog/?p=7560 Recently, I saw someone asked on Reddit what others are using these days for full-bleed and breakout elements. This refers to having a main content area of limited width (usually centered), but having the ability for some elements to be wider, either all the way to the browser edges or somewhere in-between.

desired layout at various viewports — notice the image is a full-bleed element, the warning is a breakout element and the header is a  breakout element with a full-bleed background

Is it still the old method that involves stretching elements to 100vw and then moving them in the negative direction of the x axis via an offset, margin, or translation?

Or is it the newer method that involves a grid with a limited width main column in the middle then symmetrical columns on the sides, with elements spanning an odd number of columns that depends on whether we want them to have the normal width of the main column or we want them a bit wider, breaking out of that or we even want them to be full-bleed?

There is no perfectly right answer. It depends on use case and how you look at it. We’re going to look at modified and combined versions and essentially achieve what we need to depending on the situation with modern CSS.

The old method described in the 2016 CSS-Tricks article has the disadvantage of relying on a Firefox bug (that has been fixed since 2017) to work well in all situations. The problem is that 100vw doesn’t take into account any vertical scrollbars we might have (and no, the new viewport units don’t solve that problem either). This leads to the 100vw width elements being wider than the available horizontal space if there is a vertical scrollbar, overflowing and causing a horizontal scrollbar, something I also often see with the bizarre practice of setting the width of the body to 100vw. Now, considering the elements we normally want to be full-bleed are likely images, we can hide the problem with overflow-x: hidden on the html. But it still doesn’t feel quite right.

Maybe it’s because I’m a tech, not a designer who thinks in terms of design grids, but I prefer to keep my grids minimal and when I look at the desired result, my first thought is: that’s a single column grid with the items that are wider than the column, and everything is center-aligned.

So let’s take a look at the approach I most commonly use (or at least start from), which doesn’t involve a scary-looking grid column setup, and, for the simple base cases, doesn’t involve any containers or even any calc(), which some people find confusing.

The Base Grid

We’re starting off with a grid, of course! We set a one limited width column grid on the body and we middle align this grid horizontally within the the content-box of the body:

body {
  display: grid;
  grid-template-columns: min(100%, 60em);
  justify-content: center
}

By default, display: grid creates a one column grid that stretches horizontally across the entire content-box width of the element it’s set on. This makes all the children of the element getting display: grid be distributed in that one column, one on each row. The first on the first row, the second on the second row and so on.

The grid-template-columns property is used here to max out the width of this one column at 60em by setting its width to be the minimum between 100% of the content-box width and 60em. If the content-box of the element we’ve set the grid on has a width of up to 60em, then the one column of the grid stretches horizontally across the entire content-box. If the content-box of the element we’ve set the grid on has a width above 60em, then our one grid column doesn’t stretch horizontally across the entire content-box anymore, but instead stays 60em wide, the maximum width it can take. Of course, this maximum width can be any other value we want.

The justify-content property is used to align the grid horizontally within the content-box of the element it’s set on. In this case, our one grid column is center aligned.

Note that I keep talking about the content-box here. This is because, even at really narrow viewports, we normally want a bit of space in between the text edge and the lateral edge of the available area (the viewport minus any scrollbars we might have). Initially, this space is the default margin of 8px on the body, though I also often do something similar to the approach Chris wrote about recently and zero the default margin to replace it with a clamped font-relative padding. But whichever of them is used still gets subtracted from the available space (viewport width minus any vertical scrollbar we might have) to give us the content-box width of the body.

Now whatever children the body may have (headings, paragraphs, images and so on), they’re all in the limited width grid cells of our one column, something that’s highlighted by the DevTools grid overlay in the screenshot below.

Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay.
the one limited width column grid layout with the DevTools grid lines overlay (live demo)

Full-Bleed Elements

Let’s say we want to make an element full-bleed (edge to edge). For example, an image or an image gallery, because that’s what makes the most sense to have stretching all across the entire available page width. This means we want the full viewport width minus any scrollbars we might have.

Nowadays we can get that by making the html a container so that its descendants know its available width (not including scrollbars) as 100cqw (container query width).

html { container-type: inline-size }

Having this, we can create our full-bleed elements:

.full-bleed-elem {
  justify-self: center;
  width: 100cqw
}

Setting width: 100cqw on our full-bleed elements means they get the full available content-box width of the nearest container, which is the html in this case.

The justify-self aligns the element horizontally within its grid-area (which is limited to one grid cell in our case here). We need to set it here because the default is start, which means the left edge of the element starts from the left edge of its containing grid-area. The left edge of the containing grid-area is the same as the left edge of our one column grid here.

Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay. On some of these rows, we have full-bleed elements that expand all across the entire available page width (the viewport width minus any vertical scrollbars we might have).
one column grid with full-bleed elements and a DevTools grid overlay highlighting the grid lines

Just like before, we still have a single column grid, center aligned.

One thing to note here is this means we cannot have any margin, border or padding on the html element as any of these would reduce its content-box, whose size is what the container query units are based on. In practice, the margin, border, and padding on the html are all zero by default and I don’t think I’ve seen them set to anything else anywhere outside of some mind-bending CSS Battle solutions.

Another thing to note is that there may be cases where we need another container somewhere in between. In that case, we can still access the content-box width of the html as detailed in a previous article:

@property --full-w {
  syntax: '<length>';
  initial-value: 0px;
  inherits: true;
}

html { container-type: inline-size }

body { --full-w: 100cqw }

.full-bleed-elem {
  justify-self: center;
  width: var(--full-w);
}

Often times, we probably also want some padding on the full-bleed element if it is, for example, an image gallery, but not if it is a single img element.

For img elements, the actual image always occupies just the content-box. Any padding we set on it is empty space around the content-box. This is not generally  desirable in our case. Unless we want to add some kind of decorations around it via the background property (by layering CSS gradients to create some kind of cool pattern, for example), we want the image to stretch all across the available viewport space after accounting for any vertical scrollbar we might have and not be left with empty space on the lateral sides.

Furthermore, if the img uses a box-sizing of content-box, that empty padding space gets added to the 100cqw width of its content-box, making the padding-box width exceed the available space and causing a horizontal scrollbar on the page.

When setting a padding on full-bleed elements, it’s probably best to exclude img elements:

.full-bleed-elem:not(img) { padding: .5em }

Note that in this case, the full-bleed elements getting the padding need to also have box-sizing set to border-box. This is done so that the padding gets subtracted out of the set width and not added as it would happen in the default content-box case.

.full-bleed-elem:not(img) {
  box-sizing: border-box;
  padding: .5em
}

You can see it in action and play with it in the following live demo:

You might be wondering… is it even necessary to set border-box since setting everything to border-box is a pretty popular reset style?

Personally, I don’t set that in resets anymore because I find that with the the new layout options we have, the number of cases where I still need to explicitly set dimensions in general and widths in particular has declined. Drastically. Most of the time, I just size columns, rows, set the flex property instead and let the grid or flex children get sized by those without explicitly setting any dimensions. And when I don’t have to set dimensions explicitly, the box-sizing becomes irrelevant and even problematic in some situations. So I just don’t bother with including box-sizing: border-box in the reset these days anymore and instead only set it in the cases where it’s needed.

Like here, for the non-img full bleed elements.

Another thing you may be wondering about… how about just setting a negative lateral margin?

We know the viewport width minus any scrollbars as 100cqw, we know the column width as 100%, so the difference between the two 100cqw - 100% is the space on the left side of the column plus the space on the right side of the column. This means half the difference .5*(100cqw - 100%), which we can also write as 50cqw - 50%, is the space on just one side. And then we put a minus in front and get our lateral margin. Like this:

.full-bleed-elem {
  margin: .5rem calc(50% - 50cqw);
}

Or, if we want to avoid overriding the vertical margin:

.full-bleed-elem {
  margin-inline: calc(50% - 50cqw);
}

This seems like a good option. It’s just one margin property instead of a justify-self and a width one. And it also avoids having to set box-sizing to border-box if we want a padding on our full-bleed element. But we should also take into account what exactly we are most likely to make full-bleed.

One case we considered here was that of full-bleed images. The thing with img elements is that, by default, they don’t size themselves to fit the grid areas containing them, they just use their own intrinsic size. For full-bleed images this means they are either going to not fill the entire available viewport space if their intrinsic width is smaller than the viewport or overflow the viewport if their intrinsic width is bigger than the available viewport space (the viewport width minus any vertical scrollbar we might have). So we need to set their width anyway.

For the other case, that of the scrolling image gallery, the negative margin can be an option.

Breakout Elements

These are wider than our main content, so they break out of our grid column, but are not full-bleed.

So we would give them a width that’s smaller than the content-box width of the html, which we know as 100cqw, but still bigger than the width of our only grid column, which we know as 100%. Assuming we want breakout elements to extend out on each side by 4em, this means:

.break-elem {
  justify-self: center;
  width: min(100cqw, 100% + 2*4em)
}

Again, we might use a negative lateral margin instead. For breakout elements, which are a lot more likely to be text content elements, the negative margin approach makes more sense than for the full-bleed ones. Note that just like the width, the lateral margin also needs to be capped in case the lateral space on the sides of our column drops under 4em.

.break-elem { margin: 0 max(-4em, 50% - 50cqw) }

Note that we use the max() because for negative values like the margin here, the smaller (minimum) one in absolute value (closer to 0) is the one that’s bigger when looking at the full axis going from minus to plus infinity.

But then again, we might want to be consistent and set full-bleed and breakout styles the same way, maybe grouping them together:

.full-bleed-elem, .break-elem {
  justify-self: center;
  width: min(100cqw var(--comp-w, ));
}

/* This is valid! */
.break-elem { --comp-w: , 100% + 2*4em  }

:is(.full-bleed-elem, .break-elem):not(img) {
  box-sizing: border-box;
  padding: .5em;
}

Some people prefer :where() instead of :is() for specificity reasons, as :where() always has 0 specificity, while :is() has the specificity of the most specific selector in its arguments. But that is precisely one of my main reasons for using :is() here.

And yes, both having an empty default for a CSS variable and its value starting with a comma is valid. Replacing --comp-w with its value gives us a width of min(100cqw) (which is the same as 100cqw) for full-bleed elements and one of min(100cqw, 100% + 2*4em) for breakout elements.

Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay. On some of these rows, we have full-bleed images that expand all across the entire available page width (the viewport width minus any vertical scrollbars we might have). On others, we have breakout boxes that expand laterally outside their grid cells, but are not wide enough to be full-bleed.
one column grid with full-bleed and breakout elements, as well as a DevTools grid overlay highlighting the grid lines (live demo)

If we want to have different types of breakout elements that extend out more or less, not all exactly by the same fixed value, we make that value a custom property --dx, which we can change based on the type of breakout element:

.break-elem { --comp-w: , 100% + 2*var(--dx, 4em) }

The --dx value could also be negative and, in this case, the element doesn’t really break out of the main column, it shrinks so it’s narrower.

.break-elem--mini { --dx: -2em }
.break-elem--maxi { --dx: 8em }
Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay. One of these rows has a full-bleed image that expands all across the entire available page width (the viewport width minus any vertical scrollbars we might have). On other rows, we have breakout boxes that are not the same width as their grid cells, but are not wide enough to be full-bleed. Most of these boxes are wider than their containing grid cells, but one is narrower.
one column grid with a full-bleed image and various sizes of breakout elements, as well as a DevTools grid overlay highlighting the grid lines (live demo)

Full-Bleed Backgrounds for Limited Width Elements

Sometimes we may want only the background of the element to be full-bleed, but not the element content. In the simplest case, we can do with a border-image and if you want to better understand this property, check out this article by Temani Afif detailing a lot of use cases.

.full-bleed-back {
  border-image: var(--img) fill 0/ / 0 50cqw;
}

This works for mono backgrounds (like the one created for the full-bleed header and footer below with a single stop gradient), for most gradients and even for actual images in some cases.

Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay. On the very first row, we have a limited width header with a solid full-bleed mono background. On other rows, we have full-bleed elements that expand all across the entire available page width (the viewport width minus any vertical scrollbars we might have). On other rows, we have breakout boxes that are not the same width as their grid cells, but are not wide enough to be full-bleed.
one column grid that has a tightly fit limited width header with a full-bleed mono background; it also has a full-bleed image and a breakout element, as well as a DevTools grid overlay highlighting the grid lines (live demo)

The mono background above is created as follows (all these demos adapt to user theme preferences):

--img: conic-gradient(light-dark(#ededed, #121212) 0 0)

This method is perfect for such mono backgrounds, but if we want gradient or image ones, there are some aspects we need to consider.

The thing about the 0 50cqw outset value is that it tells the browser to extend the area where the border-image is painted by 50cqw outwards from the padding-box boundary on the lateral sides. This means it extends outside the vewport, but since this is just the border-image, not the border reserving space, it doesn’t cause overflow/ a horizontal scrollbar, so we can keep it simple and use it like this for gradients.

That is, if we can avoid percentage position trouble. While this is not an issue in linear top to bottom gradients, if we want to use percentages in linear left to right gradients or to position radial or conic ones, we need to scale the [0%, 100%] interval to the [50% - 50cqw, 50% + 50cqw] interval along the x axis.

.linear-horizontal {
  --img: 
    linear-gradient(
      90deg, 
      var(--c0) calc(50% - 50cqw), 
      var(--c1) 50%
    );
}

.radial {
  --img: 
    radial-gradient(
      15cqw at calc(50% - 25cqw) 0, 
      var(--c0), 
      var(--c1)
    );
}

.conic {
  --img: 
    conic-gradient(
      at calc(50% + 15cqw), 
      var(--c1) 30%, 
      var(--c0), 
      var(--c1) 70%
    );
}

However, this scaling is not enough for linear gradients at an angle that’s not a multiple of 90°. And it may be overly complicated even for the types of gradients where it works well.

So another option is compute how much the border-image needs to expand laterally out of the available horizontal space 100cqw and the maximum grid column width --grid-w. This then allows us to use percentages normally inside any kind of gradient, including linear ones at an angle that’s not a multiple of 90°.

body {
  --grid-w: 60em;
  display: grid;
  grid-template-columns: min(100%, var(--grid-w));
  justify-content: center;
}

.full-bleed-back {
  border-image: 
    var(--img) fill 0/ / 
    0 calc(50cqw - .5*var(--grid-w));
}
Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay. On the very first row, we have a limited width header with a solid full-bleed gradient background. On other rows, we have full-bleed elements that expand all across the entire available page width (the viewport width minus any vertical scrollbars we might have). On other rows, we have breakout boxes that are not the same width as their grid cells, but are not wide enough to be full-bleed.
one column grid that has a tightly fit limited width header with a full-bleed angled gradient background (at an angle that’s not a multiple of 90°); it also has a full-bleed image and a breakout element, as well as a DevTools grid overlay highlighting the grid lines (live demo)

This has a tiny problem that other styling decisions we’re likely to take (and which we’ll discuss in a moment) prevent from happening, but, assuming we don’t make those choices, let’s take a look at it and how we can solve it.

full-bleed background issue on narrow viewports

On narrow viewports, our background isn’t full-bleed anymore, it stops a tiny distance away from the lateral sides. That tiny distance is at most the size of the lateral margin or padding on the body. As mentioned before, I prefer to zero the default margin and use a font-size-relative padding, but in a lot of cases, it doesn’t make any difference whatsoever.

Screenshot collage. Shows the top area of the page with the header in both the dark and light theme cases at a narrow viewport width of 400px. It also highlights the fact that the header's full-bleed background isn't quite full-bleed, but stops a tiny distance away from the lateral sides.
the problem in the narrow viewport case, highlighted for both the dark and the light themes

This happens when the maximum grid column width --grid-w doesn’t fit anymore in the available viewport space (not including the scrollbar) minus the lateral spacing on the sides of our one column grid (set as a margin or padding).

The solution is to use a max() instead of the calc() to ensure that the border-image expands laterally at the very least as much as that lateral spacing --grid-s.

body {
  --grid-w: 60em;
  --grid-s: .5em;
  display: grid;
  grid-template-columns: min(100%, var(--grid-w));
  justify-content: center;
  padding: 0 var(--grid-s);
}

.full-bleed-back {
  border-image: 
    var(--img) fill 0/ / 
    0 max(var(--grid-s), 50cqw - .5*var(--grid-w));
}
fix for full-bleed background issue on narrow viewports (live demo)

For actual images however, we have an even bigger problem: border-image doesn’t offer the cover option we have for backgrounds or images and we don’t really have a reliable way of getting around this. One of the repeat options might work for us in some scenarios, but I find that’s rarely the case for the results I want in such situations.

You can see the problem in this demo when resizing the viewport — for an element whose height is unknown as it depends on its content, the border-image option (the second one) means that if we want to avoid the image getting distorted, then its size needs to be intrinsic size. Always. It never scales, which means it repeats for large viewports and its sides get clipped off for small viewports.

So if we want more control over an image background or multiple background layers, it’s probably better to use an absolutely positioned pseudo-element. This also avoids the earlier problem of the full-bleed background not going all the way to the edges without taking into account the lateral spacing on the grid container (in this case, the body).

.full-bleed-back-xtra {
  position: relative;
  z-index: 1
}

.full-bleed-back-xtra::before {
  position: absolute;
  inset: 0 calc(50% - 50cqw);
  z-index: -1;
  content: ''
}

The inset makes our pseudo to stretch across the entire padding-box of its parent vertically and outside of it (minus sign) by half the available viewport space (viewport width minus any scrollbars) minus half the pseudo parent’s width.

The negative z-index on the pseudo ensures it’s behind the element’s text content. The positive z-index on the element itself ensures the pseudo doesn’t end up behind the grid container’s background too.

The pseudo background can now be a cover image:

background: var(--img-pos, var(--img) 50%)/ cover

I’m taking this approach here to allow easily overriding the background-position together with each image if necessary. In such a case, we set --img-pos:

--img-pos: url(my-back-img.jpg) 35% 65%

Otherwise, we only set --img and the default of 50% gets used:

--img-pos: url(my-back-img.jpg)

In the particular case of our demos so far, which use a light or dark theme to respect user preferences, we’ve also set a light-dark() value for the background-color, as well as an overlay blend mode to either brighten or darken our full-bleed background depending on the theme. This ensures the header text  remains readable in both scenarios.

Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay. On the very first row, we have a limited width header with a solid full-bleed image background. On other rows, we have full-bleed elements that expand all across the entire available page width (the viewport width minus any vertical scrollbars we might have). On other rows, we have breakout boxes that are not the same width as their grid cells, but are not wide enough to be full-bleed.
one column grid that has a tightly fit limited width header with a full-bleed image background; it also has a full-bleed image and a breakout element, as well as a DevTools grid overlay highlighting the grid lines (live demo)

We can also have multiple layers of gradients, maybe even blended, maybe even with a filter making them grainy (something that would help with the visible banding noticed in the border-image method examples) or creating a halftone pattern.

Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay. On the very first row, we have a limited width header with a solid full-bleed multi-gradient, filtered background. On other rows, we have full-bleed elements that expand all across the entire available page width (the viewport width minus any vertical scrollbars we might have). On other rows, we have breakout boxes that are not the same width as their grid cells, but are not wide enough to be full-bleed.
one column grid that has a tightly fit limited width header with a filtered full-bleed multi-layer background; it also has a full-bleed image and a breakout element, as well as a DevTools grid overlay highlighting the grid lines (live demo)

Combining options

We can of course also have a breakout element with a full-bleed background – in this case, we give it both classes, break-elem and full-bleed-back.

Our recipe page header for example, probably looks better as a breakout element in addition to having a full-bleed background.

If the breakout elements in general have a border or their own specific background, we should ensure these don’t apply if they also have full-bleed backgrounds:

.break-elem:not([class*='full-bleed-back']) {
  border: solid 1px;
  background: var(--break-back)
}

Or we can opt to separate these visual prettifying styles from the layout ones. For example, in the Halloween example demos, I’ve opted to set the border and background styles via a separate class .box:

.box {
  border: solid 1px var(--c);
  background: lch(from var(--c) l c h/ .15)
}

And then set --c (as well as the warning icon in front) via a .box--warn class.

Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay. On the very first row, we have a breakout header (wider than its containing grid cell, but not wide enough to be full-bleed) with a solid full-bleed multi-gradient, filtered background. On other rows, we have full-bleed elements that expand all across the entire available page width (the viewport width minus any vertical scrollbars we might have). On other rows, we have breakout boxes.
one column grid that has a breakout header with a filtered full-bleed multi-layer background; it also has a full-bleed image and a breakout element, as well as a DevTools grid overlay highlighting the grid lines (live demo)

Another thing to note here is that when having a full-bleed background for a breakout element and we use the border-image tactic, we don’t have to adapt our formula to take into account the lateral spacing, as that’s set as a padding on the breakout element and not on its grid parent.

The most important of these techniques can also be seen in the meta demo below, which has the relevant CSS in style elements that got display: block.

Nesting

We may also have a figure whose img is full-bleed, while the figcaption uses the normal column width (or maybe it’s a breakout element).

<figure>
  <img src='full-bleed-img.jpg' alt='image description' class='full-bleed-elem'>
  <figcaption>image caption</figcaption>
</figure>

Not much extra code is required here.

The simple modern solution is to make the img a block element so that the justify-self property set via the .full-bleed-elem middle aligns it even if it’s not a grid or flex item.

img.full-bleed-elem { display: block }

However, support for justify-self applying to block elements as per the current spec is still limited to only Chromium browsers at the moment. And while the Firefox bug seems to have had some activity lately, the Safari one looks like it’s dormant.

So the easy cross-browser way to get around that without any further computations is to make the figure a grid too in this case.

figure:has(.full-bleed-elem, .break-elem) {
  display: grid;
  grid-template-columns: 100%;
  width: 100%;
}
Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay. This grid has a figure that is tightly fit inside its grid cell, but also has a full-bleed image spreading across the entire available horizontal space (the viewport width minus any vertical scrollbars) we might have. On other rows, we have full-bleed elements or breakout boxes (wider than their containing grid cells, but still not wide enough to be full-bleed on wide screens). We also have a combination that's a breakout header with a full-bleed background.
one column grid that has a figure, tightly fit horizontally within its containing column, but with a full-bleed image; there’s also a DevTools grid overlay highlighting the grid lines (live demo)

Floating Problems

This is a problem that got mentioned for the three column grid technique and I really didn’t understand it at first.

I started playing with CSS to change the look of a blog and for some reason, maybe because that was what the first example I saw looked like, I got into the habit of putting any floated thumbnail and the text next to it into a wrapper. And it never occurred to me that the wrapper wasn’t necessary until I started writing this article and looked into it.

Mostly because… I almost never need to float things. I did it for those blog post thumbnails fifteen years ago, for shape-outside demos, for drop caps, but that was about it. As far as layouts go, I just used position: absolute for years before going straight to flex and grid.

This was why I didn’t understand this problem at first. I thought that if you want to float something, you have to put it in a wrapper anyway. And at the end of the day, this is the easiest solution: put the entire content of our one column in a wrapper. In which case, until justify-self applying on block elements works cross-browser, we need to replace that declaration on full-bleed and breakout elements with our old friend margin-left:

margin-left: calc(50% -50cqw)

This allows us to have floated elements inside the wrapper.

Screenshot. Shows a middle aligned grid with a single column and multiple rows, something that's highlighted by the DevTools-enabled grid overlay. This grid has a single grid child that is tightly fit inside its containing column and acts as a wrapper for full-bleed elements, breakout boxes (wider than their containing grid cells, but still not wide enough to be full-bleed on wide screens), combinations of these like a breakout header with a full-bleed background. But this wrapper also allows its children to be floated.
one column grid that has a single grid child, tightly fit horizontally within its containing column and acting as a wrapper for the entire page content; since this wrapper has no flex or grid layout, its children can be floated (live demo)

Final Thoughts: Do we even really need grid?

At this point, getting to this floats solution begs the question: do we even really need grid?

It depends.

We could just set lateral padding or margin on the body instead.

I’d normally prefer padding in this case, as padding doesn’t restrict the background and sometimes we want some full viewport backdrop effects involving both the body and the html background.

Other times, we may want a background just for the limited width of the content in the middle, in which case margin on the body makes more sense.

If we want to be ready for both situations, then we’re better off with not setting any margin or padding on the body and just wrapping all content in a limited width, middle aligned (good old max-width plus auto margins) main that also gets a background.

At the same time, my uses cases for something like this have never involved using floats and have benefitted from other grid features like gaps, which make handling spacing easier than via margins or paddings.

So at the end of the day, the best solution is going to depend on the context.

]]>
https://frontendmasters.com/blog/super-simple-full-bleed-breakout-styles/feed/ 1 7560
Using Container Query Units Relative to an Outer Container https://frontendmasters.com/blog/using-container-query-units-relative-to-an-outer-container/ https://frontendmasters.com/blog/using-container-query-units-relative-to-an-outer-container/#respond Tue, 06 May 2025 23:53:28 +0000 https://frontendmasters.com/blog/?p=5761 Recently, Matt Wilcox posted on Mastodon:

The fact you can’t specify which container for container query units is a ballache. The moment you have nested containers you’re [screwed]; because if you want the calculated gap from the row’s container; but you’re inside a nested container… tough. Your units are wrong. And you can’t just say “no; not relative to this container; relative to the named outer container!”

First off, if you’re not familiar with container queries and container query units, you can check out one of the many resources on the topic, for example this interactive guide by Ahmad Shadeed, which I believe is the most recent out of all the detailed ones I’ve seen. As always, the date of the resources used is important for web stuff, especially since these units in particular have changed their name since they were first proposed and we got an early draft of the spec.

Now, the problem at hand: let’s say we have an .inner-container inside an .outer-container – they are both made to be containers:

[class*='container'] { container-type: size }

We want any .inner-child of the .inner-container to be able to use length values set in container query units relative to the .outer-container (more precisely, to its content-box dimensions). The problem is, if we do something like this (a 20cqw light blue strip at the start of the gradient going towards 3 o’clock):

.inner-child {
  background: linear-gradient(90deg, #0a9396 20cqw, #0000)
}

… then the 20cqw value is 20% (a fifth) of the content-box width of the .inner-container. This can be seen below, where we have purple guidelines 20% of the width apart.

Screenshot illustrating how a background sized to cqw on the child of the inner container is a fifth of the inner container's width.
what 20cqw represents

But what we want is for that 20cqw value to be 20% of the content-box width of the .outer-container.

Strictly for the queries themselves, we could do something like this:

.outer-container { container: outer/ size }
.inner-container { container: inner/ size }

@container outer (min-width: 500px) {
  .inner-child { background: darkorange }
}

This allows us to set certain styles on the .inner-child elements based on where the width of the .outer-container (which isn’t the nearest container for .inner-child) is situated relative to the 500px threshold.

But we cannot do something like this to specify which container should be the one that the query units used on .inner-child are relative to:

.inner-child {
  /* can't do this */
  background: linear-gradient(90deg, #0a9396 outer 20cqw, #0000)
}

Nor can we do this:

.inner-child {
  /* can't do this either */
  --s: outer 20cqw;
  background: linear-gradient(90deg, #0a9396 var(--s), #0000)
}

However, we are getting closer!

What if we move the --s variable uspstream? After all, a 20cqw length value set on the .inner-container is 20% of the content-box width of its nearest container, which is the .outer-container. This would mean our code becomes:

[class*='container'] { container-type: size }

.inner-container {
  --s: 20cqw;
  background: 
    repeating-linear-gradient(45deg, #bb3e03 0 5px, #0000 0 1em) 
      0/ var(--s) no-repeat
}

.inner-child {
  background: 
    linear-gradient(90deg, #0a9396cc var(--s), #0000)
}

We also give the .inner-container a similar background restricted to 20cqw from the left along the x axis and make the .inner-child semi-transparent, just to check if the --s values overlap (which is what we want, --s being 20% or a fifth of the .outer-container width). However, this fails, as it can be seen below:

Screenshot. Both the inner container and its child have a background sized to 20cqw. However, the container query units are relative to the outer container only for the inner container, the container query units used on its child being still relative to the inner container (one fifth of its content-box width).
screenshot of result

For the .inner-container the 20cqw of the --s is taken to be 20% of the content-box width of its nearest container, .outer-container (dashed dark blue boundary). However, for the .inner-child, the 20cqw of the --s aren’t taken to mean the same value. Instead, they are taken to mean 20% of the .content-box width of the .inner-container (dotted dark red boundary).

Boo!

But what happens if we also register --s?

@property --s {
  syntax: '<length>';
  initial-value: 0px;
  inherits: true
}

Bingo, this works!

Screenshot. Both the inner container and its child have a background sized to 20cqw, the container query units being relative to the outer container.
desired result

I hope you’ve enjoyed this little trick.

Where would you use this?

]]>
https://frontendmasters.com/blog/using-container-query-units-relative-to-an-outer-container/feed/ 0 5761
CSS min() All The Things https://frontendmasters.com/blog/css-min-all-the-things/ https://frontendmasters.com/blog/css-min-all-the-things/#respond Wed, 23 Oct 2024 15:39:18 +0000 https://frontendmasters.com/blog/?p=4253 Victor Ayomipo saw our post about using container units for everything. He was more optimistic than I that our result was good. My thinking is that there are plenty of things you straight up don’t want to use container units for. Victor did a similar exercise with (over?)-using min() and viewport units. Turns out there are lots of niceties, and I agree that min() is underused, but that you still can’t dogmatically say one particular sizing approach is always best. Personally, I’m rocking min() for container “width”, like .wrapper { inline-size: min(300px, 100dvw); on the regular now.

]]>
https://frontendmasters.com/blog/css-min-all-the-things/feed/ 0 4253
What if you used Container Units for… everything? https://frontendmasters.com/blog/what-if-you-used-container-units-for-everything/ https://frontendmasters.com/blog/what-if-you-used-container-units-for-everything/#comments Fri, 02 Aug 2024 16:49:11 +0000 https://frontendmasters.com/blog/?p=3108 I said to myself I said what if I used container units for every single unit in a design? I was wondering, partially because I thought the answer might be well, everything will probably scale really nicely then. Container units, in case you haven’t heard of them, are unit (like px or rem, but more closely related to viewport units like vw or vi) that are sized relatively to the container that they are in.

Turns out, surprise surprise, that it’s not that easy. There are plenty of things that container queries are awkward at or just not the right unit for.

I had a play with a pretty simple grid of cards of various sizes. Don’t consider this demo in good shape, it’s just what I used to have a plan.

Potential Problem: You Can’t Style The Element You Query

This is a fairly known thing with container queries, but the weirdness with it compounds a bit with container units, as the desire to use those units right away on a container is strong. To be clear, the container units will “work”, they’ll just be based on the next-higher-up container, which if there isn’t a declared one will be the document.

.card-wrap {
  container: cardWrap / inline-size;

  padding: 2cqi;
  border-radius: 4cqi;

  .card {
    border-radius: 4cqi;
  }
}

Above, the border-radius will be different despite looking like it will be the same, because the container the units reference are different.

See how the outer border radius’ match, but the inner card are different and look off on the larger card.

Potential Solution: Style Nothing on the Container

Be vigilant! It will save headaches if you are hardfast when you set a container you do no other size-based styling. If that means adding an extra otherwise meaningless <div> wrapper, well, that’s not ultra ideal as DOM weight does matter, but it’s probably fine.

Potential Problem: Too Small & Too Big

If you only use container units for something like font-size, it’s pretty easy to get into a situation where text, and text-based elements, end up either too big or too small.

Here the text on the larger card feels a bit too big but the tags are OK. The text on the smaller card feels OK, but those tags are too small.

Either is annoying, but too small is also an accessibility failure.

Using container units (or viewport units) alone is a bad practice for text sizing. It’s fixable though.

Potential Solution: Clamp

Making sure text doesn’t get too small or too big is solved with a CSS clamp() function. And while we’re at it, we can sprinkle in a relative unit to make sure that users font sizing preferences are honored.

You can still use container units, but set those limits and use a bit of relative mixed in.

.tag {
  /* Don't */
  font-size: 4cqi;

  /* Do */
  font-size: clamp(16px, 4cqi + 0.5rem, 24px);
}

Potential Problem: Rows vs Columns

One strong use case for a container query is shifting layout at container based breakpoints. If that container goes from wide-and-short to narrow-and-tall, then the width-based (probably the most common) container units will be super different.

Say the container here is the card itself, and you size some icons to work with that context when they are horizontal. But then if you shift the layout to a narrow column for the icons, as the card gets bigger, the sizing doesn’t work in that context now.

Bonus problem: you can’t use container units to set the grid-template-columns as they can never be based on an element that is reliably the same width as the grid.

Instead, if we make the element around the icons the container, and thus it changes width when the layout changes, the change in our icon size can be too dramatic.

Potential Solution: Use a Different Unit

And here’s the rub. You just don’t have to use container units for everything. I doubt anyone ever intended for them to be used that way. It’s just a fun exercise, especially as strong scalability is a great effect.

In this case maybe something like cqmax units would be workable, so the unit is based on the containers longest edge.

.actions {
  container: actions / inline-size;

  svg {
    display: block;
    width: 4cqmax;
    min-width: 24px;
    max-width: 100%;
    aspect-ratio: 1;
  }
}

But… nah. It’s too weird. I’d say just use relative units or pixels or something here.

In the end, if container units are helpful for a scaling effect based on the size of an element you want to achieve, go for it! The support is good. But don’t force it.

If you’re up for a challenge, have a play with it. Try something like converting every unit in a more complex layout like this into containers with container units.

]]>
https://frontendmasters.com/blog/what-if-you-used-container-units-for-everything/feed/ 1 3108
Container Queries and Units https://frontendmasters.com/blog/container-queries-and-units/ https://frontendmasters.com/blog/container-queries-and-units/#respond Thu, 21 Dec 2023 15:41:02 +0000 https://frontendmasters.com/blog/?p=282 Container queries are similar to media queries but allow you set styles based on a particular element’s current size, typically the width. This is super handy because you can write CSS in a way that gives flexibility to the layout!

With @media queries, there’s a tight coupling of the styling of a component’s content and the size of the browser window. This means that the styles within a given component depend on the layout.

With @container queries, we can instead tightly couple the styling of a component’s content with the size of the component itself, regardless of how that component fits into the larger layout. In short, you can set up components to respond to the container size without having to know the breakpoints of the overall page layout. Yay for increased isolation!

Let’s think through an example to illustrate this. Pulling from Michelle Barker’s helpful MDN article about container queries, here’s a mockup:

When there’s more width available, each article preview has the image on the left and copy on the right. When there’s less room available, it stacks the image on top of the content.

Without container queries, we’d have to specify which cards we want to have the vertical layout, which ones should have the horizontal layout, and which should have a bigger image explicitly. When you then consider all possible screen sizes and container layouts, this quickly becomes quite complicated.

Additionally, if there’s a possibility for the sidebar to be collapsed or if you sometimes need to show additional content (like ads) alongside this content, it gets even more complex! Not to mention when the layout gets changed to something else, like switching from 4 columns to 3, you have to go back and adjust everything.

Container queries can help us more easily address this sort of situation in a much more manageable way!

Container queries are separate from, but can be in used in combination with, the contain property The contain property is useful for performance and preventing re-renders and, crucially, the thing that made @container queries possible under the hood.

Block and inline sizing

Before diving further into container queries, it’s important to make sure we have a good understanding of block and inline sizing as it has a large impact on the container-type and the container unit(s) we use.

Inline size is equivalent to width for horizontal writing mode and equivalent to the height for vertical writing modes. The block size is the respective opposite.

Make sure you keep this in mind! The terms “block” and “inline” are from the concept of “logical properties” and the direction CSS has been heading for a while.

How to use container queries

To use container queries, you must first define a container-type and optionally a container-name.

The container-type can have a value of size, inline-size, or normal.

  • size establishes a query container for the inline and block dimensions as well as for style (which we cover at the end of this article). 
  • inline-size establishes a query container for the inline dimensions as well as for style. You’ll likely use this 99% of the time.
  • normal establishes a query container only for style.

One potential gotcha is that if you use container-type: size you need to add an explicit height. It will ignore the height of children elements. This is how it is specced to behave.

Most often, using container-type: inline-size probably makes the most sense.

The container-name is a value of the <custom-indent> type (essentially some name you make up).

You can also use the container shorthand to define both properties. Such as:

.my-component {
  container: my-component / inline-size;
}

When using a container query or container query unit (which we will cover more in later sections), it will reference the nearest container in its ancestry tree unless you specify a particular container by including the container-name.

Once you’ve defined a container, you can use a @container query and select any descendant elements inside of it. For example:

@container (min-width: 500px) {
  .my-component p {
    font-size: 1.5rem;
  }
}

Or, if you want to use the container name in the query:

@container my-component (min-width: 500px) {
  .my-component p {
    font-size: 1.5rem;
  }
}

Note that you cannot style the element that is being queried inside of the container query itself (like .my-component {} in this case). But you can use it as a part of a more complex selector as seen above.

But you don’t have to refer to the container element in the selector, meaning this is also valid:

@container my-component (min-width: 500px) {
  p {
    font-size: 1.5rem;
  }
}

You can also use nesting.

.my-component {
  @container (min-width: 500px) {
    p {
      font-size: 1.5rem;
    }
  }
}

orientation and aspect-ratio

Instead of using explicit container sizes for container queries, we can also make use of orientation and its more generic form, aspect-ratio.

For example, here’s a Pen where we have the image on the left for larger screens and on top for smaller screens (a non-aspect ratio version of this sort of thing is in the section below).

When using aspect-ratio, remember that it’s width divided by the height, so aspect-ratio < 1/1 would be when the width is larger than the height (this example is equivalent to orientation: landscape). You can also use min-aspect-ratio or max-aspect-ratio instead of plain aspect-ratio and a comparison symbol.

Note that orientation and aspect-ratio can only be used with a container-type of size because it uses the container’s width and height. Setting a height is not typically a great idea for any template-based design where content can change.

What are container query units?

Container query units are an addition to the container query specification that provides units based on the container’s dimensions. This is handy for sizing pieces of a component based on the component’s container size.

What’s more, you’re not restricted to using container query units inside of container queries. You can use them anywhere a container is specified! That means that in some cases you can get away with just setting a property’s value to something that uses a container query unit and just leave it at that.

A shortened name for container query units?

“Container query units” is kind of a mouthful to say. Given that they can be used outside of container queries (so long as a container is defined), I think we can refer to these as “container units” like Chris Coyier did when he wrote about them a while back. I’m going to call them that for the rest of this article.

Available container units

Here’s the list of container units we currently have access to:

  • cqw: 1% of a query container’s width
  • cqh: 1% of a query container’s height
  • cqi: 1% of a query container’s inline size
  • cqb: 1% of a query container’s block size
  • cqmin: The smaller value of either cqi or cqb
  • cqmax: The larger value of either cqi or cqb

The width and height values are pretty straightforward to use. However, keep in mind that cqh will only use a container height if the container has a container-type of size. If inline-size is used, it will base its height on the nearest container with container-type: size, which might be the viewport.

Of these units, cqi will probably be the most commonly used unit for those who want to build websites for international audiences. For horizontal languages, it is equivalent to cqw. But it automatically switches to use cqh for vertical languages.

If you’re still not sure what inline means vs block here, maybe spend more time in the section above.

Use cases for container queries and container units

Let’s take a quick look at some use cases for container queries and container units!

Changing a component’s layout based on how much space is available

Perhaps the most common use case for container queries is to change the layout of a component’s contents based on the container’s size. Paired with ways of doing layouts like flex and grid, it can make creating pages that respond to different viewport sizes even easier.

Accessibility note: It’s best to keep the logical order of elements in the markup.

Taken to an extreme, you can make HTML and CSS components function kinda like an SVG like Dan Christofi did:

Adding non-vital detail when there’s more space available

In addition to changing the layout, sometimes it makes sense to hide some of the less important information or decorative elements when a component is smaller.

A great example of this is Chris Coyier’s calendar layout demo, where the vital parts of the calendar are kept for the smallest size but the rest is hidden:

(You may want to open this one full screen to have play.)

Fluid typography

Fluid typography is the concept of defining font sizes in a way where the type automatically scales based on some dimension between pre-defined bounds. In the past this has been based on the viewport width, but container query units make this concept a lot more powerful!

Check out this demo by Chris Coyier where you can drag to divvy up width between two elements, both with responsive type sizes:

Stephanie Eckles wrote a more in-depth article about using container query units for typography that I highly recommend!

When to use media queries instead

Content queries and units free us up from having to always use breakpoints that are tied to the layout. However, there are cases where you want content to update based on the layout! That’s when you should still use media queries—so content can be updated across multiple components at the same time.

Another time to use media queries is when you’re wanting to check certain device features, such as @media (not(hover)) { ... } or @media (not (color)) { ... } (which checks if the display is monochrome).

Browser support and style queries

Container queries for sizing have pretty solid browser support these days, as do container units.

There’s also discussion around creating style container queries. This would make certain things easier, like alternating between nested italic and normal text. Since the values of CSS variables can also be used in style queries, they could also be used as a more legitimate alternative to the CSS variable/custom property toggle hack. But at the moment they are only partially supported in WebKit-based browsers.

Conclusion

Container queries and container units enable us to create more isolated components. This makes it easier for components to be used across multiple pages, layouts, and systems. They’re prime for use in design systems!

If you’re interested in what other new CSS features I used when recreating my blog, check out my blog post about the process.

Bonus demo

This demo by SitePoint shows responsive layout paired with container queries to inspire you further!

]]>
https://frontendmasters.com/blog/container-queries-and-units/feed/ 0 282