How Well Do You Know CSS Layout?
The difference between a CSS good experience and a long frustrating one is oftentimes a matter of a few small details. CSS is indeed nuanced. One of the most common areas where I see struggles is layout. Personally, I like to study patterns. I notice that I tend to use a small group of patterns to solve the majority of my layout problems. This article is about those CSS patterns I use to get myself through layout challenges. It is also about approaching situations agnostically, regardless of the CSS methodologies used, whether that’s SMACSS, BEM, or even the hot topic of CSS-in-JS because they all focus on the properties themselves rather than architecture, organization, or strategy.
Just for fun, let’s start with a test
We’ll use a platform that I happen to have made called Questionable.io and I’ve used it to create a test that we’ll get to below. Don’t worry, there is no personal data collected, results are anonymous and it’s totally free.
The purpose of the test is to see if you can recognize specific CSS behaviors and problems in context without first being presented with the material. I didn’t set out to make the test difficult, but CSS layout nuances tend to be somewhat complex, especially without having a lot of exposure to them. Remember, this all for fun. The results are not an indication of your awesomeness, but hopefully you get value out of it.
The test is 10 questions and should 10 minutes or less.
Interested in the test but don’t want to take it? Here’s a link to the questions with their correct answers.
Done already? Great! Let’s go over the questions one-by-one to get a better understanding of the layout patterns that are covered in the test.
Question 1: Box Model
Learning the Box Model should be high priority on anyone’s list. While this CSS-Tricks Box Model Article may be a bit old, don’t underestimate its value and relevance to modern CSS. The Box Model is prerequisite knowledge for almost every CSS topic related to layout.
This particular question is testing how to get the Box Model’s computed width. The box clearly has width: 100px;
but it turns out that the default rules of the Box Model apply width
properties to the content layer of the box. The computed width (how wide is rendered on the page) is the sum of the content layer, padding layer, and border layer. For this reason, the answer is 112px:
.box {
width: 100px; /* Take this */
height: 50px;
padding: 5px; /* Plus this x2 for left and right */
border: 1px solid red; /* Plus this x2 for left and right */
background-color: red;
/* = 112px of computed width */
}
If you’ve encountered a situation where the last column or tab in a UI wraps down to the next line and you were confident that five tabs (all set to width: 20%;
) adds up to 100%, then it’s very possible that this was the issue. Five tabs at 20% width does add up to 100%, but if there’s padding and/or borders involved, those will add width there won’t be room for the last tab to fit on the same line.
Around the time of CSS3 being introduced, a new tool called box-sizing
came to CSS. This allows us to change what layer of the Box Model we want width
to apply. For example, we can do box-sizing: border-box;
which means we want any width
rules to apply to the outside of the border layer instead of the content layer. In this test question, if box-sizing: border-box;
had been applied, the computed width would have been 100px.
This is old news for some of you but a good reminder for pros and novices alike.
There are a number of articles on the Box Model and how to use box-sizing as a reset, so it’s applied to your entire project all at once. Box Sizing and Inheriting box-sizing Probably Slightly Better Best-Practice are two great articles right on CSS-Tricks to get started.
Question 2: Borders are pushy
The second test question could almost be considered “Part Two” of the first question. Remember, it’s one thing to read, “The Box Model has layers and they all contribute to the calculated width and hight.” It’s another to be able to recognize a Box Model problem in a real situation. This particular problem is somewhat of a classic among those who have been doing CSS for a while. It stems from the fact that borders take up space and will push things around since they are a part of the Box Model. Introducing borders during a state-transition, like :hover
, will mean that boxes get bigger and thus push subsequent boxes down. It can also create a jittery experience:
See the Pen CSS-Tricks: Borders are Dimension by Brad Westfall (@bradwestfall) on CodePen.
Out of all the possible solutions in the test question, doing border: 2px solid transparent
on the initial “un-hovered” state would be the only one that fixes the problem. box-sizing
doesn’t fix this problem because we are not explicitly setting a height. If we had, then the border would be factored on the inside of the height and there would be no shift — but this wasn’t the case.
There are also other solutions that weren’t mentioned as possible answers. One is faux borders with box-shadow
and the other is to use outline
instead of border
. Either of those would have resulting in no shifting during state changes because they are not layers in the Box Model. Here’s another CSS-Tricks article to read more about these solutions
Keep in mind that outline
does not support border-radius
.
Question 3: Absolute position vs. fixed position
Aside from knowing when to use each and how they differ in visual behavior, it’s also very important to know the rules for how each positioning method attaches to a parent element with its top
, right
, bottom
, or left
properties.
First, let’s review Containing Block. The short definition is that a Containing Block is most often the parent of any given element. However, the rules for Containing Block are different between absolute and fixed elements:
1. For absolute elements: The Containing Block is the nearest ancestor parent that is not static
. For example, when an element is absolute-positioned, and contains top
, right
, bottom
, or left
properties, it will position relative to any parent that has a position of absolute
, relative
, fixed
, or sticky
.
2. For fixed elements: The Containing Block is the viewport, regardless of any parents that have position values other than static
. Also, the scrolling behavior is different than absolute
in that position: fixed;
elements stay “fixed” to the viewport as it scrolls, hence the name.
Many developers believe absolute-positioned elements only seek the nearest position: relative;
parent. This is a common misconception simply because position: relative
is most often paired with position: absolute;
to make a Containing Block. The reason it’s commonly used is because relative
keeps the parent in flow which is often the desirable behavior. There are times though that the Containing Block of an absolute positioned element is also absolute positioned. This is totally okay depending on the situation. If all parents are static, then the absolute positioned element will attach to the viewport — but in a way that scrolls with the viewport:
See the Pen CSS-Tricks: Position Absolute Scrolling by Brad Westfall (@bradwestfall) on CodePen.
There is a lesser-known caveat to the two rules above: Anytime a parent has a transform
property (among a few others) with a value other than none
, then that parent will become the Containing Block for absolute- and fixed-positioned elements. This can be observed in this Pen where the notice is position: fixed;
and the parent has transform
but only when hovered:
See the Pen CSS-Tricks: Containing Blocks by Brad Westfall (@bradwestfall) on CodePen.
Question 4: Parent and first/last child collapsing margins
This is one of those CSS details that can really bite you if you don’t know how it works. There is a CSS concept called Collapsing Margins and many people are familiar with the form of it called Adjacent Siblings Collapsing Margins. However, there is another form of it called Parent and First/Last Child Collapsing Margins which is lesser known. Here is a demo of both:
See the Pen CSS-Tricks: Collapsing Margins by Brad Westfall (@bradwestfall) on CodePen.
Each paragraph tag has a top and bottom margin of 1em that are provided by the browser. So far, that’s the easy part. But why is the gap between the paragraphs not 2em (the sum of the top and bottom)? This is called Adjacent Sibling Collapsing Margins. The margins overlap such that the larger of the two margins will be the total gap size, thus the gap in this case is 1em.
There’s something else happening that’s a little strange though. Did you notice that the top margin of the first paragraph doesn’t create a gap between it and the blue container div? Instead of a gap, it’s almost like it “contributes” the margin to the parent div as if the div had the top margin. This is called Parent and First/Last Child Collapsing Margins. This form of Collapsing Margins will not happen in some circumstances if the parent has any of these:
- Top/Bottom padding of any value bigger than 0.
- Top/Bottom border of any width bigger than 0.
- Block Formatting Context, which can be created by things like
overflow: hidden;
andoverflow: auto;
). display: flow-root
(not well supported).
When I have the pleasure of explaining this small CSS detail to people and solving it with padding or border, the response is almost always, “what about padding or border of 0?” Well, that doesn’t work because the value must be a positive integer.
In the previous example, just 1px of padding allows us to toggle between using and preventing Parent/Child Collapsing Margins. The gap that shows up between the first/last paragraphs and the parent is the 1px of padding but now the margin is being factored to the inside of the container since the padding layer creates a barrier preventing collapsing margins.
Regarding the question, I’m confident you can see what the problem is in this UI:
See the Pen CSS-Tricks: Parent/Child Collapsing Margins by Brad Westfall (@bradwestfall) on CodePen.
The first .comment
(without the .moderator
class) is experiencing Collapsing Margins. Even without looking at the code, we can see that the moderator comment has a border and the non-moderator one does not. In the question, there were actually three answers that were considered correct. Each one is actually already applied in the source of the Pen, they’re just commented out.
One reason why this form of Collapsing Margins isn’t as widely known as the others is the wide array of ways we can “accidentally” avoid it. Flexbox and grid items create a Block Formatting Context, so we don’t see this form of Collapsing Margins there. If our “comments” UI were a real project, chances are we would have had padding on all four coordinates to create spacing all the way around, which would fix any Collapsing Margins for us. As rare as it might be, I wouldn’t want you to spend a whole day scratching you head on this one, so it’s good to keep in your thoughts when working with layout.
Here are some CSS-Tricks articles on this subject:
Question 5: Percent of what?
When it comes to using percentage units, the percent is said to be based on the Containing Block’s width or height (usually related to the parent). As we stated earlier, an element with transform
will become a Containing Block, so when an element is using transform
, the percentage units (for transform
only) are based on its own size rather than the parent.
In this example, we can see that 50% means two different things depending on context. The first red block has margin-left: 50%;
and the second red block is using transform: translateX(50%);
:
See the Pen CSS-Tricks – Percentage and Transform by Brad Westfall (@bradwestfall) on CodePen.
Question 6: The Box Model strikes again… what a hangover!
Just when you thought we were done talking about Box Model…
See the Pen CSS-Tricks: Left: 0 Right: 0 by Brad Westfall (@bradwestfall) on CodePen.
The hangover stems from the fact that we are using width: 100%;
on the footer and also adding padding. The container is 500px wide which means the footer’s content layer (being 100%) is 500px wide before padding is applied to the outside of that layer.
The hangover can be fixed with one of these two common techniques:
- Use
box-sizing
on the footer directly or via a reset, lie we discussed earlier. - Remove the
width
and doleft: 0; right: 0;
instead. This is a great use case for doing aleft
value and aright
value at the same time. Doing so will avoid Box Model issues because thewidth
will use its default valueauto
to take up any available space between paddings and borders whenleft: 0; right: 0;
are set.
One of the options was “Remove the padding on the footer.” This would technically work to fix the hangover because the content layer being 100% would have no padding or border to expand it beyond the width of the container. But I think this solution is the wrong approach because we shouldn’t have to change our UI to accommodate Box Model issues that are easily avoided.
The reality for me is that I always have box-sizing: content-box;
as apart of my reset. If you also do this, then perhaps you don’t see this problem often. But I still like to do the left: 0; right: 0;
trick anyways because, over time, it has been more stable (at least in my experience) than having to deal with Box Model issues arising from width: 100%;
on positioned elements.
Question 7: Centering absolute and fixed elements
Now we’re really starting to combine all the material from above with the centering of absolute and fixed elements:
See the Pen CSS-Tricks: Modal (Lightbox) Centering by Brad Westfall (@bradwestfall) on CodePen.
Since we’ve already covered most of the material in this test question, I’ll simply point out that horizontal and vertical centering can be done “the old school way” with negative margins or the newer “kinda old school but still good” way of doing transforms. Here is an amazing CSS-Tricks guide on all things centering.
It used to be said that if we know the width and height of the box, the we should use negative margins because they’re more stable than transitions, which were new to browsers. Now that transitions are stable, I use them almost all the time for this, unless I need to avoid a Containing Block.
Also know that we can’t use any margin: auto;
tricks for this because we need modals to “hover” over the content which is why position
is typically used to them out of Normal Flow.
Speaking of which, let’s move on to the next question, which deals with centering with Normal Flow.
Question 8: Centering elements with Normal Flow
Flexbox brought us many amazing tools for solving difficult layout problems. Before it’s release, it was said that vertical centering was one of the most difficult things to do in CSS. Now it’s somewhat trivial:
.parent { display: flex; }
.child { margin: auto; }
See the Pen CSS-Tricks: Flexbox Centering (Vertical and Horizontal) by Brad Westfall (@bradwestfall) on CodePen.
Notice that with flexbox items, the margin: auto
is being applied to top, right, bottom, and left to center vertically and horizontally. Doing vertical centering with auto
didn’t work in the past with block-level elements which is why doing margin: 0 auto
is common.
Question 9: Calculate mixed units
Using calc()
is perfect when two units that we can’t add up on our own need to be mixed or when we need to make fractions easier to read. This test question asks us to figure out what calc(100% + 1em)
would be based on the fact that the width of the div is 100px. The correct answer is that the calculated value is 100px plus whatever 1em is in the browser. We don’t know what 1em will be when we write our CSS because it’s context-dependent — thus the reason why calc()
is needed in the first place.
There are a few key places where I see myself regularly reaching for calc()
. One is anytime I want to offset something by 100% but also add a fixed amount of extra space. Dropdown menus can be a good example of this:
See the Pen CSS Tricks: Calculate Mixed Units by Brad Westfall (@bradwestfall) on CodePen.
The trick here is that we want to make a “dropdown system” where the dropdown menu can be used with different trigger sizes (in this case, two different size buttons). We don’t know what the height of the trigger will be but we do know that top: 100%;
will placed at the top of our menu and at the very bottom of the trigger. If every menu needs to be at the bottom of their respective trigger, plus .5em, then that can happen with top: calc(100% + 0.5em);
. Sure, we could use top: 110%;
as well, but that extra 10% would be context-dependent based on the height of the trigger and the container.
Question 10: Negative margins
Unlike positive margins that push away from their siblings, negative margins pull them closer together without moving the sibling elements. This final test question offers two solutions that technically work to eliminate the double border in our button group, but I strongly prefer the negative margins technique because removing borders would make it much more challenging to do certain tricks like this hover effect:
See the Pen CSS Tricks: Negative Margins with Button Groups by Brad Westfall (@bradwestfall) on CodePen.
The effect is a “common border” that is shown between the buttons. Buttons can’t actually share a common border so we need this negative margin trick to make the two borders overlap. Then I’m using a z-index
to manage which border I want to be on top depending on the hover state. Note that z-index
is useful here even without absolute positioning, but I did have to do position: relative
. If I had used the technique to remove the left border of the second button, this effect would have been more difficult to pull off.
It all adds up!
There is one last demo I want to show you that utilizes many tricks we’ve discussed so far. The task is to create UI tiles that expand all the way to the left and right edges of the container with gutters. By tiles, I mean the ability to have a list of blocks that wraps down to the next line when there’s no more space. Hover over the tiles to see the full effect:
See the Pen CSS-Tricks: Flexbox Tiles (Edge-to-Edge) by Brad Westfall (@bradwestfall) on CodePen.
The hurdle with this task is the gutters. Without gutters, it would be trivial to get the tiles to touch the left and right edge of the container. The problem is that the gutters will be created by margin, and when we add margin to all sides of the tile, we create two problems:
- Having three tiles with
width: 33.33%;
in combination with margin will mean three tiles cannot fit on one row. Whilebox-sizing
will allow us to have padding and borders on the.tile
which will be contained within33.33%
, it will not help us with margins — that means the computed width of the three tiles will be more than 100%, forcing the last one down to the next line. - Tiles on the far left and right side will no longer touch the edges of the container.
The first problem can be solved with calc((100% / 3) - 1em)
. That’s 33.33% minus the left and right margins of each tile. Adjacent Sibling Collapsing Margins don’t apply here because there is no such thing as Collapsing Margins when it comes to left and right margin. As a result, the horizontal distance between each tile is the sum of the two margins (1em). It also doesn’t apply in this case with the top and bottom margins because the first tile and the fourth tile are technically not Adjacent Siblings, even though they happen to be right next to each other visually.
With the calc()
trick, three tiles are able to fit on a row, but they still don’t extend to edges of the container. For that, we can use negative margins in the amount equal to the left and right margin of each tile. The green dotted line in the example is the container where we will apply negative margins to draw out the tiles to match the edge of the surrounding content. We can see that how it extends into its parent’s padding area (the main
element). That’s okay because negative margins don’t push neighboring elements around.
The end result is that the tiles have nice gutters that extend edge-to-edge so that they align to the neighboring paragraph tags outside the tiles.
There’s a lot of ways to solve tiles (and they usually come with their own pros and cons). For example, there’s a rather elegant solution using CSS Grid discussed by Heydon Pickering which is responsive using a technique that mimics container queries (but with Grid magic). Ultimately, his Grid solution to tiles is much nicer than the flexbox solution I presented, but it also has less browser support. Nonetheless, the flexbox solution is still a great way to demo all the tricks from this article at the same time.
You may already be familiar with Heydon’s work. He’s known for creating clever tricks like the Lobotomized Owl selector. If you’re not familiar, it’s certainly worth knowing and I have a video where I talk about it.
Summary
I stated at the start that I tend to look for patterns when solving problems. This article isn’t necessarily about the exact demo scenarios from above; it’s more about a set of tools that can be used to solve these and many other layout problems that we are all likely to come across. I hope these tools take you far and I look forward to hearing your contributions in the comments.
By the way, there are a number of excellent resources that cover the Box Model in thorough detail, most notably ones by Rachel Andrews and Jen Simmons that are certainly worth checking out. Rachel even has a newsletter completely dedicated to layout.
- Box Alignment Cheatsheet – Great resource with visuals that highlight the various properties that affect how elements are aligned, either by themselves or relative to other elements.
- Jen Simmons Labs – A slew of helpful posts, demos and experiments using modern layout methods.
The post How Well Do You Know CSS Layout? appeared first on CSS-Tricks.