đź‘‹ This article has been expanded into a new project called Defensive CSS. Due to the fact that the content here will not be updated, I recommend you to read defensivecss.dev instead.
Oftentimes, we wish that there was a way to avoid a certain CSS issue or behaviors from happening. You know, content is dynamic, and things can change on a web page, thus increasing the possibility of a CSS issue or a weird behavior.
Defensive CSS is a collection of snippets that can help you in writing CSS that is protected. In other words, you will have fewer issues in the future. If you follow my blog, you might read an article I wrote a while back which is called “The just in case mindset”. This is built upon it, and will be an ongoing list of snippets. If you have any suggestions, please feel free to let me know about it.
Table of contents
- Flexbox wrapping
- Spacing
- Long content
- Prevent an image from being stretched or compressed
- Lock scroll chaining
- CSS variable fallback
- Using fixed width or height
- Forgetting
background-repeat
- Vertical media queries
- Using
justify-content: space-between
- Text over images
- Be careful with fixed values in a CSS grid
- Show a scrollbar only when it's needed
- Scrollbar gutter
- Minimum content size in CSS flexbox
- Minimum content size in CSS grid
- Auto fit vs auto fill
- Image maximum width
- position: sticky css grid
- Grouping selectors
Flexbox wrapping
CSS flexbox is one of the most useful CSS layout features nowadays. It’s tempting to add display: flex
to a wrapper and have the child items ordered next to each other.
The thing is when there is not enough space, those child items won’t wrap into a new line by default. We need to change that behavior with flex-wrap: wrap
.
Here is a typical example. We have a group of options that should be displayed next to each other.
.options-list {
display: flex;
}
When there is less space, horizontal scrolling will occur. That’s should be expected and isn’t actually a “problem”.
Notice how the items are still next to each other. To fix that, we need to allow flex wrapping.
.options-list {
display: flex;
flex-wrap: wrap;
}
A general rule of thumb when using flexbox is to allow wrapping unless you want a scrolling wrapper. That’s another thing, but try to use flex-wrap
to avoid unexpected layout behaviors (horizontal scrolling, in our case).
Spacing
We developers need to account for different content lengths. That means, spacing should be added to a component, even though it seems like not needed.
In this example, we have a section title and an action button on the right side. Currently, it looks okay. But let’s see what happens when the title is longer.
Notice how the text is too close to the action button? You might be thinking about multi-line wrapping, but I will come to that in another section. For now, let’s focus on the spacing.
If the title has spacing and text truncation, we won’t see such an issue.
.section__title {
margin-right: 1rem;
}
Long content
Accounting for long content is important when building layout. As you might saw in the previous, the section title is truncated when it’s too long. That is optional, but for some UIs it’s important to account for that.
For me, this is a defensive CSS approach. It’s nice to get to fix the “problem” before it actually happens.
Here is a list of people’s names and it looks perfect for now.
However, since this is user-generated content, we need to be careful about how to defense the layout in case of something too long.
See the following figure:
In such layouts, consistency is important. To achieve that, we can simply truncate the name by using text-overflow
and its friends.
.username {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
If you’re interested to sharpen your skills in handling long content in CSS, I wrote a detailed article on that topic.
Prevent an image from being stretched or compressed
When we don’t have control over an image’s aspect ratio on a web page, it’s better to think ahead and provide a solution when a user uploads an image that isn’t aligned with the aspect ratio.
In the following example, we have a card component with a photo. It looks good.
When the user uploads an image of a different size, it will be stretched. This isn’t good. Look at how the image is stretched!
The simplest fix for that is to use CSS object-fit
.
.card__thumb {
object-fit: cover;
}
On a project level, I prefer to add object-fit
to all images to avoid unexpected image results.
img {
object-fit: cover;
}
Learn more about object-fit
in this article on Smashing Magazine.
Lock scroll chaining
Have you ever opened a modal and started scrolling, and then when you reach the end and keep scrolling, the content underneath the modal (the body element) will scroll? This is called scroll chaining.
There have been a few hacks to make this work in the past years, but now, we can do that with CSS only, thanks to the ‌overscroll-behavior
CSS property.
In the following figure, you see the default scroll chaining behavior.
To avoid that ahead of time, we can add that to any component that needs to scroll (e.g: chat component, mobile menu.. etc). The nice thing about this property is that it won’t have an effect until there is scrolling.
.modal__content {
overscroll-behavior-y: contain;
overflow-y: auto;
}
In case you want to learn more about it, I wrote a detailed article on that.
CSS variable fallback
CSS variables are gaining more and more usage in web design. There is a method that we can apply to use them in a way that doesn’t break the experience, in case the CSS variable value was empty for some reason.
This is particularly useful when feeding the value of a CSS variable via Javascript. Here is an example:
.message__bubble {
max-width: calc(100% - var(--actions-width));
}
The variable --actions-width
is being used within the calc()
function and its value is coming from Javascript. Let’s suppose that Javascript failed for some reason, what will happen? The max-width
will compute to none
.
We can avoid that ahead of time and add a fallback value to the var()
.
.message__bubble {
max-width: calc(100% - var(--actions-width, 70px));
}
That way, if the variable isn’t defined, the fallback (70px
) will be used. This approach can be used in case there is a possibility that the variable might fail (e.g: coming from Javascript). Otherwise, it’s not needed.
Using fixed width or height
One of the common things that break a layout is using a fixed width or height with an element that has content in different lengths.
The fixed height
I often see a hero section with a fixed height and content that is larger than that height, which results in a broken layout. Not sure how that looks? Here it is.
.hero {
height: 350px;
}
To avoid the content leaking out of the hero, we need to use min-height
instead of height
.
.hero {
min-height: 350px;
}
That way, if the content gets larger, the layout won’t break.
The fixed width
Have you ever seen a button that has its label too close to the left and right edges? This is due to using a fixed width.
.button {
width: 100px;
}
If the button’s label is longer than 100px
, it will be close to the edges. If it’s too long, the text will leak out of it. This isn’t good!
To fix that, we can simply replace width
with min-width
.
.button {
min-width: 100px;
}
Forgetting background-repeat
Oftentimes, when using a large image as a background, we tend to forget to account for the case when the design is viewed on a large screen. That background will repeat by default.
This mostly won’t be visible on a laptop screen, but it can be seen clearly on larger screens.
To avoid that behavior in advance, make sure to reset background-repeat
.
.hero {
background-image: url("..");
background-repeat: no-repeat;
}
Vertical media queries
Sometimes, it’s so tempting to build a component and only test by resizing the browser’s width. Testing against the browser’s height can reveal some interesting problems.
Here is one that I’ve seen multiple times. We have an aside component with main and secondary links. The secondary links should be positioned at the very bottom of the aside section.
Consider the following example. The main and secondary navigation looks okay. In the example that I saw, the developer added position: sticky
to the secondary navigation so that it can stick to the bottom.
However, if the browser height is smaller, things will break. Notice how the two navigations are overlapped.
By using CSS vertical media queries, we can avoid that issue.
@media (min-height: 600px) {
.aside__secondary {
position: sticky;
bottom: 0;
}
}
That way, the secondary navigation will only be sticked to the bottom if the viewport height is larger than or equals 600px. Much better, right?
There are probably better ways to implement that behavior (like using margin-auto
) but I’m focusing on the vertical query for this example.
If I want to explain using CSS vertical media query, I need to write a full article about it. The good news is that I already wrote one, in case you’re interested.
Using justify-content: space-between
In a flex container, you might use justify-content
to space the child items from each other. With a certain number of child items, the layout will look okay. However, when they increase or decrease, the layout will look odd.
Consider the following example.
We have a flex container with four items. The spacing between each item isn’t a gap
or margin
, it’s there because the container has justify-content: space-between
.
.wrapper {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
When the number of items is less than four, here is what will happen.
This isn’t good. There are different solutions to this:
- Margin
- Flexbox gap (Use with caution)
- Padding (Can be applied to the parent of each child element)
- Adding empty elements to act as a spacer.
For simplicity, I will use gap
.
.wrapper {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
Text over images
When using the text over an imaging approach, it’s important to account for the case where the image fails to load. How the text will look like?
Here is an example.
The text looks readable, but when the image fails to load, it won’t.
We fix that easily by adding a background color to the <img>
element. This background will only be visible if the image fails to load. Isn’t that cool?
.card__img {
background-color: grey;
}
Be careful with fixed values in a CSS grid
Say we have a grid that contains an aside and main. The CSS looks like this:
.wrapper {
display: grid;
grid-template-columns: 250px 1fr;
gap: 1rem;
}
This will break on small viewport sizes due to the lack of space. To avoid such an issue, always use a media query when using CSS grid like the above.
@media (min-width: 600px) {
.wrapper {
display: grid;
grid-template-columns: 250px 1fr;
gap: 1rem;
}
}
Show a scrollbar only when it’s needed
Luckily, we can control to show a scrollbar or not only in the case of having a long content. That being said, it’s highly recommended to use auto
as a value for overflow
.
Consider the following example.
Notice how even if the content is short, there is a scrollbar visible. This isn’t good for a UI. As a designer, it’s just confusing to see a scrollbar when it’s not needed.
.element {
overflow-y: auto;
}
With overflow-y: auto
, the scrollbar will only be visible if the content is long. Otherwise, it won’t be there. Here is an updated figure.
Scrollbar gutter
Another thing that is related to scrolling is the scrollbar gutter. Taking the previous example, when the content gets longer, adding a scrollbar will cause a layout shift. The reason the layout shift happens is to reserve a space for the scrollbar.
Consider the following figure.
Notice how the content shifted when it became longer as a result of showing a scrollbar. We can avoid that behavior by using the scrollbar-gutter
property.
.element {
scrollbar-gutter: stable;
}
Minimum content size in CSS flexbox
If a flex item has a text element or an image that is bigger than is longer than the item itself, the browser won’t shrink them. That is the default behavior for flexbox.
Consider the following example.
.card {
display: flex;
}
When the title has a very long word, it won’t wrap into a new line.
Even if we use overflow-wrap: break-word
, it won’t work.
.card__title {
overflow-wrap: break-word;
}
To change that default behavior, we need to set the min-width
of the flex item to 0
. That’s because the min-width
default value is auto
, the overflow happens.
.card__title {
overflow-wrap: break-word;
min-width: 0;
}
The same thing applies to a column flex wrapper, but we will use min-height: 0
instead.
Minimum content size in CSS grid
Similar to flexbox, CSS grid has a default minimum content size for its child items which is auto
. That means, if there is an element that is larger than the grid item, it will overflow.
In the example above, we have a carousel within the main section. For context, here is the HTML and CSS.
<div class="wrapper">
<main>
<section class="carousel"></section>
</main>
<aside></aside>
</div>
@media (min-width: 1020px) {
.wrapper {
display: grid;
grid-template-columns: 1fr 248px;
grid-gap: 40px;
}
}
.carousel {
display: flex;
overflow-x: auto;
}
Since the carousel is a flex container that doesn’t wrap, its width is larger than the main section, and thus the grid item respected that. As a result, there is horizontal scrolling.
To fix that, we have three different solutions:
- Using
minmax()
- Applying
min-width
to the grid item - Adding
overflow: hidden
to the grid item
As a defensive CSS mechanism, I would go for the first one which is using the minmax()
function.
@media (min-width: 1020px) {
.wrapper {
display: grid;
grid-template-columns: minmax(0, 1fr) 248px;
grid-gap: 40px;
}
}
I wrote about that early this year. I also highly recommend checking “Preventing a Grid Blowout” and “You want minmax(10px, 1fr) not 1fr” by Chris Coyier.
Auto fit vs auto fill
When using CSS grid minmax()
function, it’s important to decide between using the auto-fit
or auto-fill
keywords. When used incorrectly, it can lead to unexpected results.
When using minmax()
function, the auto-fit
keyword will expand the grid items to fill the available space. While auto-fill
will keep the available space reserved without altering the grid items width.
That being said, using auto-fit
might lead to grid items being too wide, especially when they are less than expected. Consider the following example.
.wrapper {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 1rem;
}
If there is only one grid item and auto-fit
is used, the item will expand to fill the container width.
Most of the time, such behavior isn’t needed, so using auto-fill
is better in my opinion.
.wrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 1rem;
}
Image maximum width
As a general rule them, don’t forget to set max-width: 100%
to all images. This can be added to the CSS reset that you use.
img {
max-width: 100%;
object-fit: cover;
}
position: sticky css grid
Have you ever tried using position: sticky
with a child of a grid container? The default behavior for grid items is to stretch. As a result, the aside element in the example below is equal to the main section height.
To make it work as expected, you need to reset align-self
property.
aside {
align-self: start;
position: sticky;
top: 1rem;
}
I wrote about that topic in detail on my blog, if you’re interested.
Grouping selectors
It’s not recommended to group selectors that are meant to work with different browsers. For example, styling an input’s placeholder needs multiple selectors per the browser. If we group the selectors, the entire rule will be invalid, according to w3c.
/* Don't do this, please */
input::-webkit-input-placeholder,
input:-moz-placeholder {
color: #222;
}
Instead, do this.
input::-webkit-input-placeholder {
color: #222;
}
input:-moz-placeholder {
color: #222;
}
It’s not the end!
That’s not the end, but I really enjoyed documenting all these techniques. This is an ongoing list of defensive CSS techniques that I personally use depending on the project I’m working on. If you have anything to suggest, please get in touch via Twitter @shadeed9.
Interested in more content like that?
I wrote an ebook on everything about Debugging CSS.
If you’re interested, head over to debuggingcss.com for a free preview.