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 take 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, like 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: border-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, then 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. This was a little tricky because it actually doesn’t matter that the div is 100px wide. The percent is based on the parent’s width so the answer is Whatever 100% of the containing block’s (parent’s) width is plus 1em.
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.
I believe the answer to question 9 is incorrect.
margin-left:100% will not be 100px (the div width) it will be the width of the containing block (assume the viewport in this case). Therefore the answer should be the width of the containing:block + 1 em.
Unless I’m mistaken :)
You are correct. I updated the question and the article to reflect. Thanks for the find, Paul!
The questionnaire (https://app.questionable.io/test-review/527/pZnrnE6z) site didn’t allow any clicks/selections of answers in either Firefox or Chrome on Linux Mint. Fyi…
Thanks Vanderson, that’s the link for “test review” mode. It doesn’t allow for highlighting/copying code. Neither does testing mode actually. The reason is that Questionable.io is most often used for assessment while hiring and so this is meant to deter test takers from easily looking up answers through copy/paste.
I’m a bit confused with the Question #9. Shouldn’t the correct answer be “the width of the element’s container plus 1em”, not the width of the element itself plus 1em? Here is the example where the element with given styles has different resulting margin than the answer suggests: http://jsfiddle.net/kt9q3x5z/
Yea, Paul pointed that out above. It’s fixed now. Thanks, Ilya
Re: Question 7, I thought transform/translate could result in blurry edges and differently-rendered text (maybe there was no sub-pixel anti aliasing?). So it’s great for animations where subtle blurring isn’t noticed, but not for static elements like that modal. But the modal looks perfect. Am I misremembering? No longer a problem?
I think it does in some cases, but not all. Can’t remember exactly
#Question 6 is misleading.
The question asks “Which of these solutions fixes the hangover?” And “Remove the padding on the footer” definitely fixes the hangover so it should not be regarded as an incorrect answer.
If you are asking for an answer that does not change the UI, make it clear in the question that the UI should not be changed.
I appreciate the feedback Ian, really! But I just don’t agree. If a solution changes UI in a drastic negative way, it’s not a good solution especially if another solution that is so easy doesn’t change the UI at all. Also, remember it’s just for fun and the questions weren’t meant to be the takeaway here, the content is
also, if there’s any padding on the container or is content, using “left: 0; right: 0;” would still cause the footer to be larger than it’s visible content
Am I just not understanding what you mean by “Use calc() to measure out the center with top and left” on question 7? Because swapping the negative margins for
calc
ed left/top properties is also a solution to that problem.Hi Jesse, sorry it was confusing. Now that I think about it, your right. I’ll come up with a better “wrong” answer
I got 1 wrong and didn’t realize one of the earlier questions allowed multiple answered. Only one of the later questions had instructions clearly indicating I could choose more than one answer.
Nice test, got 9 out of 12 right.
Would have been great to show the multiple answerable questions with a checkbox and not a radio button, I guess many people will miss this.
This got me, too. Radio buttons strongly imply only one answer may be chosen.
Thanks for the article and fun test :)
Hopefully you don’t mind my suggestions to improve the test a bit :)
Firstly:
I can’t get my head around question 4; Thinking and thinking and thinking, I can’t come up with another explanation other than: You must have made a mistake :D
How can a margin on an element cause it’s own background and border to expand?
And why would it only happen in the yellow, not the blue?
The pen supports my claim, which relieved me quite a bit :D https://codepen.io/katerlouis/pen/dwrxaQ – please enlighten me if I’m missing something here.
Question 6 is a bit unfair and should be phrased more specific. You never say the padding on the footer is a requirement, so technically removing the padding does fix the overhang. But this is not a real life solution. Of course the padding is required and you rightfully so say this is not a correct answer. But the question doesn’t allow that in my opnion :)
with question 7 I find it confusing that both valid solutions, answer 1 (transform) and answer 3 (calc()) cannot both be picked together, like you allow in some other questions. That may make users unsure; but I’m wondering if that is intentional.
I know this should be just a small test and fun side thing to warm up for the article; but if you’d like to improve it I suggest considering to add design deliverables to the “real world” examples (like the menu item jump in question 2 and the footer overhang in question 6).
I think The correct/best solution of the menu jump requires/is given by a design decision.
– Transparent border on non-hover add extra vertical space between the items
– Blue border (same as background) would add more more visual padding to the button
– border-style inset on hover would take visual padding away from the button
So while this surely is a code question, it can only be answered by asking the design question first.
This may seem nitpicky to some; but in my opinion these details are crucially important
The way I see it (and luckily my agency aswell) good results can only come if all parties work as close together as possible.
Hi René.
4. It’s correct. In your codepen, go to settings and take off your css reset. Not sure what reset that is but it’s probably taking away margin on H1 by default.
6. I just answered in someone else’s comment
7. I meant to fix that last night, sorry
Cannot access the quiz, clicking the link brings me to a Questionable.io error page that says “We apologize, but we’re experiencing some errors.”
I was able to answer the set of questions correctly, yet the “score” at the end showed only 83% and “1” point.
Why would that occur?
One of the questions had several correct answers, each worth a fraction of a point
Great test with some nice small “gotcha” moments, specially the second one where box-sizing: border-box; would normally work but requires a explicit width to be defined
I don’t know my CSS layout too well, I’m still trying to figure out how to remove unused CSS from my page because it Google Page Speed is telling me it’s slowing down my site.
On the second question, wouldn’t moving the border to the initial state solve the problem, I know it would change the initial state visually but it would solve the jumping wouldn’t it?
Great article (and test). I’m currently preparing for a job interview, so this could have come in a better.
Cheers, I’ve learned a lot!
Yup. I don’t know anything.
These are some of the difficult parts of CSS. I’m sure you know good stuff. It’s all about continuing to learn more
I got 8 out of 10 with plenty of time to spare. I really didn’t want to answer the first question because the box model wasn’t dictated, so I felt a bit trapped, but I should have realized it would have been the default box model that was being implied. Doh!
And the second was the vertical/horizontal positioning with Flexbox + margin: auto. I was not aware this was possible actually, so I learned something new! I thought using align-items and justify-content were required for VERTICAL centering (I knew it would work for horizontal).
Thanks for posting this! That was fun.
Nice one. Bit irked that the last question only got me a half point.
Another issue with removing a border like that is that you don’t get a nice rectangle – if the border is thicker or you have a high dpi screen the edges of the open border will be angled / creating visual artifacts.
Love the test, Brad! Great work! I believe there’s a bug in the wording to the correct answer for #5. The answer choice says “half the red box’s width (50px)”, but the box is 50×50 so half of its width is 25px.
Fixed. thanks
Awesome test.
I think question 2 is a bit misleading in its wording at the very end. In my opinion, “keeping the design the same”, means keeping the button size unchanged. Adding a border, however, does make them some pixels higher. This led me to choose the wrong answer, although I knew that the issue could be solved by adding borders.
Adding a fixed height and adjusting the box sizing will keep the design “the same” in this case.
Just dropping a quick link to my dumb little Twitter quiz from a few weeks ago here because it’s kinda relevant:
Nice test, but please never ever style checkboxes as radio’s ever again. I was in the assumption i could only tick one answer per question. ;-)
Still did it great a job, only had those partially (in)correct.
The issue with showing a more “checkbox-like” UI with questions that have multiple correct answers has been published. Sorry for any confusion
Technically speaking, fixed positioning (regarding question #3) positions an element relative to the viewport unless an ancestor “has a transform, perspective, or filter property set to something other than none.” Source: https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed
This has bitten me more than I care to admit…