In CSS, line-height is probably one of the most misunderstood, yet commonly-used attributes. As designers and developers, when we think about line-height, we might think about the concept of leading from print design — a term, interestingly enough, that comes from literally putting pieces of lead between lines of type.
Leading and line-height, however similar, have some important differences. To understand those differences, we first have to understand a bit more about typography.
An overview of typography terms
In traditional Western type design, a line of text is comprised of several parts:
Baseline: This is the imaginary line on which the type sits. When you write in a ruled notebook, the baseline is the line on which you write.
Descender: This line sits just below the baseline. It is the line that some characters — like lowercase g, j, q, y and p — touch below the baseline.
X-height: This is (unsurprisingly) the height of a normal, lowercase x in a line of text. Generally, this is the height of other lowercase letters, although some may have parts of their characters that will exceed the x-height. For all intents and purposes, it servers as the perceived height of lowercase letters.
Cap-height: This is the height of most capital letters on a given line of text.
Ascender: A line that oftentimes appears just above the cap height where some characters like a lowercase h or b might exceed the normal cap height.
Each of the parts of text described above are intrinsic to the font itself. A font is designed with each of these parts in mind; however, there are some parts of typography that are left up to the type setter (like you and me!) rather than the designer. One of these is leading.
Leading is defined as the distance between two baselines in a set of type.
A CSS developer might think, “OK, leading is the line-height, let’s move on.” While the two are related, they are also different in some very important ways.
Let’s take a blank document and add a classic “CSS reset” to it:
* {
margin: 0;
padding: 0;
}
This removes the margin and padding from every single element.
We will need some content, so let’s an create an <h1> tag with some text and set the line-height to something obnoxiously huge, like 300px. The result is a single line of text with a surprising amount of space both above and below the single line of text.
CodePen Embed Fallback
When a browser encounters the line-height property, what it actually does is take the line of text and place it in the middle of a “line box” which has a height matching the element’s line-height. Instead of setting the leading on a font, we get something akin to padding one either side of the line box.
As illustrated above, the line box wraps around a line of text where leading is created by using space below one line of text and above the next. This means that for every text element on a page there will be half of the leading above the first line of text and after the last line of text in a particular text block.
What might be more surprising is that explicitly setting the line-height and font-size on an element with the same value will leave extra room above and below the text. We can see this by adding a background color to our elements.
CodePen Embed Fallback
This is because even though the font-size is set to 32px, the actual text size is something less than that value because of the generated spacing.
Getting CSS to treat line-height like leading
If we want CSS to use a more traditional type setting style instead of the line box, we’ll want a single line of text to have no space either above or below it — but allow for multi-line elements to maintain their entire line-height value.
It is possible to teach CSS about leading with a little bit of effort. Michael Taranto released a tool called Basekick that solves this very issue. It does so by applying a negative top margin to the ::before pseudo-elementand a translateY to the element itself. The end result is a line of text without any extra space around it.
The most up-to-date version of Basekick’s formula can be found in the source code for the Braid Design System from SEEK. In the example below, we are writing a Sass mixin to do the heavy lifting for us, but the same formula can be used with JavaScript, Less, PostCSS mixins, or anything else that provides these kinds of math features.
At first glance, this code definitely looks like a lot of magic numbers cobbled together. But it can be broken down considerably by thinking of it in the context of a particular system. Let’s take a look at what we need to know:
$baseFontSize:This is the normal font-size for our system around which everything else will be managed. We’ll use 16px as the default value.
$typeSizeModifier: This is a multiplier that is used in conjunction with the base font size to determine the font-size rule. For example, a value of 2 coupled with our base font size of 16px will give us font-size: 32px.
$descenderHeightScale: This is the height of the font’s descender expressed as a ratio. For Lato, this seems to be around 0.11.
$capHeight: This is the font’s specific cap height expressed as a ratio. For Lato, this is around 0.75.
$gridRowHeight: Layouts generally rely on default a vertical rhythm to make a nice and consistently spaced reading experience. For example, all elements on a page might be spaced apart in multiples of four or five pixels. We’ll be using 4 as the value because it divides easily into our $baseFontSize of 16px.
$typeRowSpan: Like $typeSizeModifier, this variable serves as a multiplier to be used with the grid row height to determine the rule’s line-height value. If our default grid row height is 4 and our type row span is 8, that would leave us with line-height: 32px.
Now we can then plug those numbers into the Basekick formula above (with the help of SCSS functions and mixins) and that will give us the result below.
CodePen Embed Fallback
That’s just what we’re looking for. For any set of text block elements without margins, the two elements should bump against each other. This way, any margins set between the two elements will be pixel perfect because they won’t be fighting with the line box spacing.
Refining our code
Instead of dumping all of our code into a single SCSS mixin, let’s organize it a bit better. If we’re thinking in terms of systems, will notice that there are three types of variables we are working with:
Variable Type
Description
Mixin Variables
System Level
These values are properties of the design system we’re working with.
$baseFontSize $gridRowHeight
Font Level
These values are intrinsic to the font we’re using. There might be some guessing and tweaking involved to get the perfect numbers.
$descenderHeightScale $capHeight
Rule Level
These values will are specific to the CSS rule we’re creating
$typeSizeMultiplier $typeRowSpan
Thinking in these terms will help us scale our system much easier. Let’s take each group in turn.
First off, the system level variables can be set globally as those are unlikely to change during the course of our project. That reduces the number of variables in our main mixin to four:
$baseFontSize: 16;
$gridRowHeight: 4;
@mixin basekick($typeSizeModifier, $typeRowSpan, $descenderHeightScale, $capHeight) {
/* Same as above */
}
We also know that the font level variables are specific to their given font family. That means it would be easy enough to create a higher-order mixin that sets those as constants:
Now, on a rule basis, we can call the Lato mixin with little fuss:
.heading--medium {
@include Lato(2, 10);
}
That output gives us a rule that uses the Lato font with a font-size of 32px and a line-height of 40px with all of the relevant translates and margins. This allows us to write simple style rules and utilize the grid consistency that designers are accustomed to when using tools like Sketch and Figma.
As a result, we can easily create pixel-perfect designs with little fuss. See how well the example aligns to our base 4px grid below. (You’ll likely have to zoom in to see the grid.)
CodePen Embed Fallback
Doing this gives us a unique superpower when it comes to creating layouts on our websites: We can, for the first time in history, actually create pixel-perfect pages. Couple this technique with some basic layout components and we can begin creating pages in the same way we would in a design tool.
CodePen Embed Fallback
Moving toward a standard
While teaching CSS to behave more like our design tools does take a little effort, there is potentially good news on the horizon. An addition to the CSS specification has been proposed to toggle this behavior natively. The proposal, as it stands now, would add an additional property to text elements similar to line-height-trim or leading-trim.
One of the amazing things about web languages is that we all have an ability to participate. If this seems like a feature you would like to see as part of CSS, you have the ability to drop in and add a comment to that thread to let your voice be heard.
Just like there is a movement to make more websites (and more of websites) from pre-rendered static files (Jamstack), so to might we consider moving content-based APIs to be static. Sean C Davis:
A static API is simply a collection of flat JSON files that live on a content delivery network (CDN). It doesn’t perform any action other than delivering content (static JSON files) to the requesting user.
But that doesn’t mean a static API is simple. And every file doesn’t have to be manually generated or updated. A static API can still use a database and it can still pull data from external services. In other words, it can be dynamically generated, but it is statically delivered.
The use cases are probably somewhat limited, but I still like it. In a sense, all RSS feeds (or, more directly, JSON feeds) could be done this way, and many are.
Compared to the past, modern browsers have become really efficient at rendering the tangled web of HTML, CSS, and JavaScript code a typical webpage provides. It takes a mere milliseconds to render the code we give it into something people can use.
What could we, as front-end developers, do to actually help the browser be even faster at rendering? There are the usual best practices that are so easy to forget with our modern tooling — especially in cases where we may not have as much control over generated code. We could keep our CSS under control, for instance, with fewer and simpler selectors. We could keep our HTML under control; keep the tree flatter with fewer nodes, and especially fewer children. We could keep our JavaScript under control; while being careful with our HTML and CSS manipulations.
Actually, modern frameworks such as Vue and React do help out a good bit with that last part.
I would like to explore a CSS property that we could use to help the browser figure out what calculations it can reduce in priority or maybe even skip altogether.
This property is called contain. Here is how MDN defines this property:
The contain CSS property allows an author to indicate that an element and its contents are, as much as possible, independent of the rest of the document tree. This allows the browser to recalculate layout, style, paint, size, or any combination of them for a limited area of the DOM and not the entire page, leading to obvious performance benefits.
A simple way to look at what this property provides is that we can give hints to the browser about the relationships of the various elements on the page. Not necessarily smaller elements, such as paragraphs or links, but larger groups such as sections or articles. Essentially, we’re talking about container elements that hold content — even content that can be dynamic in nature. Think of a typical SPA where dynamic content is being inserted and removed throughout the page, often independent of other content on the page.
A browser cannot predict the future of layout changes to the webpage that can happen from JavaScript inserting and removing content on the page. Even simple things as inserting a class name to an element, animating a DOM element, or just getting the dimensions of an element can cause a reflow and repaint of the page. Such things can be expensive and should be avoided, or at least be reduced as much as possible.
Developers can sort of predict the future because they’ll know about possible future changes based on the UX of the page design, such as when the user clicks on a button it will call for data to be inserted in a div located somewhere in the current view. We know that’s a possibility, but the browser does not. We also know that there’s a distinct possibility that inserting data in that div will not change anything visually, or otherwise, for other elements on the page.
Browser developers have spent a good amount of time optimizing the browser to handle such situations. There are various ways of helping the browser be more efficient in such situations, but more direct hints would be helpful. The contain property gives us a way to provide these hints.
The various ways to contain
The contain property has three values that can be used individually or in combination with one another: size, layout, and paint. It also has two shorthand values for common combinations: strict and content. Let’s cover the basics of each.
Please keep in mind that there are a number of rules and edge cases for each of these that are covered in the spec. I would imagine these will not be of much concern in most situations. Yet, if you get an undesired result, then a quick look at the spec might be handy.
There is also a style containment type in the spec that this article will not cover. The reason being that the style containment type is considered of little value at this time and is currently at-risk of being removed from the spec.
Size containment
size containment is actually a simple one to explain. When a container with this containment is involved in the layout calculations, the browser can skip quite a bit because it ignores the children of that container. It is expected the container will have a set height and width; otherwise, it collapses, and that is the only thing considered in layout of the page. It is treated as if it has no content whatsoever.
Consider that descendants can affect their container in terms of size, depending on the styles of the container. This has to be considered when calculating layout; with size containment, it most likely will not be considered. Once the container’s size has been resolved in relation to the page, then the layout of its descendants will be calculated.
size containment doesn’t really provide much in the way of optimizations. It is usually combined with one of the other values.
Although, one benefit it could provide is helping with JavaScript that alters the descendants of the container based on the size of the container, such as a container query type situation. In some circumstances, altering descendants based on the container’s size can cause the container to change size after the change was done to the descendants. Since a change in the container’s size can trigger another change in the descendants you could end up with a loop of changes. size containment can help prevent that loop.
Here’s a totally contrived example of this resizing loop concept:
CodePen Embed Fallback
In this example, clicking the start button will cause the red box to start growing, based on the size of the purple parent box, plus five pixels. As the purple box adjusts in size, a resize observer tells the red square to again resize based on the size of the parent. This causes the parent to resize again and so on. The code stops this process once the parent gets above 300 pixels to prevent the infinite loop.
The reset button, of course, puts everything back into place.
Clicking the checkbox “set size containment” sets different dimensions and the size containment on the purple box. Now when you click on the start button, the red box will resize itself based on the width of the purple box. True, it overflows the parent, but the point is that it only resizes the one time and stops; there’s no longer a loop.
If you click on the resize container button, the purple box will grow wider. After the delay, the red box will resize itself accordingly. Clicking the button again returns the purple box back to its original size and then the red box will resize again.
While it is possible to accomplish this behavior without use of the containment, you will miss out on the benefits. If this is a situation that can happen often in the page the containment helps out with page layout calculations. When the descendants change internal to the containment, the rest of the page behaves as if the changes never happened.
Layout containment
layout containment tells the browser that external elements neither affect the internal layout of the container element, nor does the internal layout of the container element affect external elements. So when the browser does layout calculations, it can assume that the various elements that have the layout containment won’t affect other elements. This can lower the amount of calculations that need to be done.
Another benefit is that related calculations could be delayed or lowered in priority if the container is off-screen or obscured. An example the spec provides is:
[…] for example, if the containing box is near the end of a block container, and you’re viewing the beginning of the block container
The container with layout containment becomes a containing box for absolute or fixed position descendants. This would be the same as applying a relative position to the container. So, keep that in mind how the container’s descendants may be affected when applying this type of containment.
On a similar note, the container gets a new stacking context, so z-index can be used the same as if a relative, absolute, or fixed position was applied. Although, setting the top, right, bottom, or left properties has no effect on the container.
Here’s a simple example of this:
CodePen Embed Fallback
Click the box and layout containment is toggled. When layout containment is applied, the two purple lines, which are absolute positioned, will shift to inside the purple box. This is because the purple box becomes a containing block with the containment. Another thing to note is that the container is now stacked on top of the green lines. This is because the container now has a new stacking context and follows those rules accordingly.
Paint containment
paint containment tells the browser none of the children of the container will ever be painted outside the boundaries of the container’s box dimensions. This is similar to placing overflow: hidden; on the container, but with a few differences.
For one, the container gets the same treatment as it does under layout containment: it becomes a containing block with its own stacking context. So, having positioned children inside paint containment will respect the container in terms of placement. If we were to duplicate the layout containment demo above but use paint containment instead, the outcome would be much the same. The difference being that the purple lines would not overflow the container when containment is applied, but would be clipped at the container’s border-box.
Another interesting benefit of paint containment is that the browser can skip that element’s descendants in paint calculations if it can detect that the container itself is not visible within the viewport. If the container is not in the viewport or obscured in some way, then it’s a guarantee that its descendants are not visible as well. As an example, think of a nav menu that normally sits off-screen to the left of the page and it slides in when a button is clicked. When that menu is in its normal state off-screen, the browser just skips trying to paint its contents.
Containments working together
These three containments provide different ways of influencing parts of rendering calculations performed by the browser. size containment tells the browser that this container should not cause positional shifting on the page when its contents change. layout containment tells the browser that this container’s descendants should not cause layout changes in elements outside of its container and vice-versa. paint containment tells the browser that this container’s content will never be painted outside of the container’s dimensions and, if the container is obscured, then don’t bother painting the contents at all.
Since each of these provide different optimizations, it would make sense to combine some of them. The spec actually allows for that. For example, we could use layout and paint together as values of the contain property like this:
.el {
contain: layout paint;
}
Since this is such an obvious thing to do, the spec actually provides two shorthand values:
Shorthand
Longhand
content
layout paint
strict
layout paint size
The content value will be the most common to use in a web project with a number of dynamic elements, such as large multiple containers that have content changing over time or from user activity.
The strict value would be useful for containers that have a defined size that will never change, even if the content changes. Once in place, it’ll stay the intended size. A simple example of that is a div that contains third-party external advertising content, at industry-defined dimensions, that has no relation to anything else on the page DOM-wise.
Performance benefits
This part of the article is tough to explain. The problem is that there isn’t much in the way of visuals about the performance benefits. Most of the benefits are behind-the-scenes optimizations that help the browser decide what to do on a layout or paint change.
As an attempt to show the contain property’s performance benefits, I made a simple example that changes the font-size on an element with several children. This sort of change would normally trigger a re-layout, which would also lead to a repaint of the page. The example covers the contain values of none, content, and strict.
CodePen Embed Fallback
The radio buttons change the value of the contain property being applied to the purple box in the center. The “change font-size” button will toggle the font-size of the contents of the purple box by switching classes. Unfortunately, this class change is also a potential trigger for re-layout. If you’re curious, here is a list of situations in JavaScript and then a similar list for CSS that trigger such layout and paint calculations. I bet there’s more than you think.
My totally unscientific process was to select the contain type, start a performance recording in Chome’s developer tools, click the button, wait for the font-size change, then stop the recording after another second or so. I did this three times for each containment type to be able to compare multiple recordings. The numbers for this type of comparison are in the low milliseconds each, but there’s enough of a difference to get a feel for the benefits. The numbers could potentially be quite different in a more real-world situation.
But there are a few things to note other than just the raw numbers.
When looking through the recording, I would find the relevant area in the timeline and focus there to select the task that covers the change. Then I would look at the event log of the task to see the details. The logged events were: recalculate style, layout, update layer tree, paint, and composite layers. Adding the times of all those gives us the total time of the task.
The event log with no containment.
One thing to note for the two containment types is that the paint event was logged twice. I’ll get back to that in a moment.
Completing the task at hand
Here are the total times for the three containment types, three runs each:
Containment
Run 1
Run 2
Run 3
Average
none
24 ms
33.8 ms
23.3 ms
27.03 ms
content
13.2 ms
9 ms
9.2 ms
10.47 ms
strict
5.6 ms
18.9 ms
8.5 ms
11 ms
The majority of the time was spent in layout. There were spikes here and there throughout the numbers, but remember that these are unscientific anecdotal results. In fact, the second run of strict containment had a much higher result than the other two runs; I just kept it in because such things do happen in the real world. Perhaps the music I was listening to at the time changed songs during that run, who knows. But you can see that the other two runs were much quicker.
So, by these numbers you can start to see that the contain property helps the browser render more efficiently. Now imagine my one small change being multiplied over the many changes made to the DOM and styling of a typical dynamic web page.
Where things get more interesting is in the details of the paint event.
Layout once, paint twice
Stick with me here. I promise it will make sense.
I’m going to use the demo above as the basis for the following descriptions. If you wish to follow along then go to the full version of the demo and open the DevTools. Note that you have to open up the details of the “frame” and not the “main” timeline once you run the performance tool to see what I’m about to describe.
Showing frame details open and main details closed in DevTools
I’m actually taking screenshots from the “fullpage” version since DevTools works better with that version. That said, the regular “full” version should give roughly the same idea.
The paint event only fired once in the event log for the task that had no containment at all. Typically, the event didn’t take too long, ranging from 0.2 ms to 3.6 ms. The deeper details is where it gets interesting. In those details, it notes that the area of paint was the entire page. In the event log, if you hover on the paint event, DevTools will even highlight the area of the page that was painted. The dimensions in this case will be whatever the size of your browser viewport happens to be. It will also note the layer root of the paint.
Paint event details
Note that the page area to the left in the image is highlighted, even outside of the purple box. Over to the right, are the dimensions of the paint to the screen. That’s roughly the size of the viewport in this instance. For a future comparison, note the #document as the layer root.
Keep in mind that browsers have the concept of layers for certain elements to help with painting. Layers are usually for elements that may overlap each other due to a new stacking context. An example of this is the way applying position: relative; and z-index: 1; to an element will cause the browser to create that element as a new layer. The contain property has the same effect.
There is a section in DevTools called “rendering” and it provides various tools to see how the browser renders the page. When selecting the checkbox named “Layer borders” we can see different things based on the containment. When the containment is none then you should see no layers beyond the typical static web page layers. Select content or strict and you can see the purple box get converted to its own layer and the rest of the layers for the page shift accordingly.
Layers with no containmentLayers with containment
It may be hard to notice in the image, but after selecting content containment the purple box becomes its own layer and the page has a shift in layers behind the box. Also notice that in the top image the layer line goes across on top of the box, while in the second image the layer line is below the box.
I mentioned before that both content and strict causes the paint to fire twice. This is because two painting processes are done for two different reasons. In my demo the first event is for the purple box and the second is for the contents of the purple box.
Typically the first event will paint the purple box and report the dimensions of that box as part of the event. The box is now its own layer and enjoys the benefits that applies.
The second event is for the contents of the box since they are scrolling elements. As the spec explains; since the stacking context is guaranteed, scrolling elements can be painted into a single GPU layer. The dimensions reported in the second event is taller, the height of the scrolling elements. Possibly even narrower to make room for the scrollbar.
First paint event with content containmentSecond paint event with content containment
Note the difference in dimensions on the right of both of those images. Also, the layer root for both of those events is main.change instead of the #document seen above. The purple box is a main element, so only that element was painted as opposed as to whole document. You can see the box being highlighted as opposed to the whole page.
The benefits of this is that normally when scrolling elements come into view, they have to be painted. Scrolling elements in containment have already been painted and don’t require it again when coming into view. So we get some scrolling optimizations as well.
Again, this can be seen in the demo.
Back to that Rendering tab. This time, check “Scrolling performance issue” instead. When the containment is set to none, Chrome covers the purple box with an overlay that’s labeled “repaints on scroll.”
DevTools showing “repaints on scroll” with no containment
If you wish to see this happen live, check the “Paint flashing” option.
Please note: if flashing colors on the screen may present an issue for you in some way, please consider not checking the “Paint flashing” option. In the example I just described, not much changes on the page, but if one were to leave that checked and visited other sites, then reactions may be different.
With paint flashing enabled, you should see a paint indicator covering all the text within the purple box whenever you scroll inside it. Now change the containment to content or strict and then scroll again. After the first initial paint flash it should never reappear, but the scrollbar does show indications of painting while scrolling.
Paint flashing enabled and scrolling with no containmentPaint flashing and scrolling with content containment
Also notice that the “repaints on scroll” overlay is gone on both forms of containment. In this case, containment has given us not only some performance boost in painting but in scrolling as well.
An interesting accidental discovery
As I was experimenting with the demo above and finding out how the paint and scrolling performance aspects worked, I came across an interesting issue. In one test, I had a simple box in the center of page, but with minimal styling. It was essentially an element that scrolls with lots of text content. I was applying content containment to the container element, but I wasn’t seeing the scrolling performance benefits described above.
The container was flagged with the “repaints on scroll” overlay and the paint flashing was the same as no containment applied, even though I knew for a fact that content containment was being applied to the container. So I started comparing my simple test against the more styled version I discussed above.
I eventually saw that if the background-color of the container is transparent, then the containment scroll performance benefits do not happen.
I ran a similar performance test where I would change the font-size of the contents to trigger the re-layout and repaint. Both tests had roughly the same results, with only difference that the first test had a transparent background-color and the second test had a proper background-color. By the numbers, it looks like the behind-the-scenes calculations are still more performant; only the paint events are different. It appears the element doesn’t become its own layer in the paint calculations with a transparent background-color.
The first test run only had one paint event in the event log. The second test run had the two paint events as I would expect. Without that background color, it seems the browser decides to skip the layer aspect of the containment. I even found that faking transparency by using the same color as the color behind the element works as well. My guess is if the container’s background is transparent then it must rely on whatever is underneath, making it impossible to separate the container to its own paint layer.
I made another version of the test demo that changes the background-color of the container element from transparent to the same color used for the background color of the body. Here are two screenshots showing the differences when using the various options in the Rendering panel in DevTools.
Rendering panel with transparent background-color
You can see the checkboxes that have been selected and the result to the container. Even with a content containment applied, the box has “repaints on scroll” as well as the green overlay showing painting while scrolling.
Rendering panel with background-color applied
In the second image, you can see that the same checkboxes are selected and a different result to the container. The “repaints on scroll” overlay is gone and the green overlay for painting is also gone. You can see the paint overlay on the scrollbar to show it was active.
Conclusion: make sure to apply some form of background color to your container when applying containment to get all the benefits.
Here’s what I used for the test:
CodePen Embed Fallback
This is the bottom of the page
This article has covered the basics of the CSS contain property with its values, benefits, and potential performance gains. There are some excellent benefits to applying this property to certain elements in HTML; which elements need this applied is up to you. At least, that’s what I gather since I’m unaware of any specific guidance. The general idea is apply it to elements that are containers of other elements, especially those with some form of dynamic aspect to them.
Some possible scenarios: grid areas of a CSS grid, elements containing third-party content, and containers that have dynamic content based on user interaction. There shouldn’t be any harm in using the property in these cases, assuming you aren’t trying to contain an element that does, in fact, rely in some way on another element outside that containment.
Browser support is very strong. Safari is the only holdout at this point. You can still use the property regardless because the browser simply skips over that code without error if it doesn’t understand the property or its value.
It’s probably one part coronavirus, one part new-fancy-video setup, and one part “hey this is good for CodePen too,” but I’ve been doing more videos lately. It’s nice to be back in the swing of that for a minute. There’s something fun about coming back to an old familiar workflow.
Where do the videos get published? I’m a publish-on-your-own site kinda guy, as I’m sure you know, so there is a whole Videos section of this site where every video we’ve ever published lives. There is also a YouTube channel, of course, which is probably the most practical way for most people to subscribe. We’re about halfway to Wes Bos-level, so let’s go people!
I had literally forgotten about it, but ages ago when I set this up, I created a special RSS feed for the videos so I could submit it as a video podcast on iTunes. That’s all still there and working! An interesting side note is that this enables offline viewing, as most podcatchers can cache subscriptions. Why build an app when you get the core ability for free, right?
I keep the original videos, of course. On individual video pages, I show a YouTube player that could be somewhat easily swapped out for another player if something crazy happened, like YouTube closes down or drastically changed their business model in some way that makes it problematic to show videos with their player. The originals are stored in an S3 bucket. If you’re an MVP Supporter, I give you the original high-quality download link right on the video pages.
If your curious about my workflow, I’m still using ScreenFlow. I don’t make nearly enough use of it, but it feels good in that it’s fairly easy to use, very reliable and fast, and I can always learn and do more with it. Shooting my screen is easy and a built-in feature of ScreenFlow of course. I also have a Rode Podcaster on a boom arm at my desk so the audio is passable. And I just went through a whole process to use a DSLR camera at my desk too, and I think the quality from that is great. It’s all a little funny because I have this whole sound recording booth as well, with a $1,000 audio setup in there, but I only use that for podcasting. The lighting sucks in there, making it no good for video.
It’s this new desk setup that has inspired me to do more video, and I suspect it will continue! One thing I could really use is a new high quality intro video. Just like a five-second thing with refreshed aesthetics. Anyone do that kind of work?
Integration tests are a natural fit for interactive websites, like ones you might build with React. They validate how a user interacts with your app without the overhead of end-to-end testing.
This article follows an exercise that starts with a simple website, validates behavior with unit and integration tests, and demonstrates how integration testing delivers greater value from fewer lines of code. The content assumes a familiarity with React and testing in JavaScript. Experience with Jest and React Testing Library is helpful but not required.
There are three types of tests:
Unit tests verify one piece of code in isolation. They are easy to write, but can miss the big picture.
End-to-end tests (E2E) use an automation framework — such as Cypress or Selenium — to interact with your site like a user: loading pages, filling out forms, clicking buttons, etc. They are generally slower to write and run, but closely match the real user experience.
Integration tests fall somewhere in between. They validate how multiple units of your application work together but are more lightweight than E2E tests. Jest, for example, comes with a few built-in utilities to facilitate integration testing; Jest uses jsdom under the hood to emulate common browser APIs with less overhead than automation, and its robust mocking tools can stub out external API calls.
Another wrinkle: In React apps, unit and integration are written the same way, with the same tools.
Getting started with React tests
I created a simple React app (available on GitHub) with a login form. I wired this up to reqres.in, a handy API I found for testing front-end projects.
You can log in successfully:
…or encounter an error message from the API:
The code is structured like this:
LoginModule/
├── components/
⎪ ├── Login.js // renders LoginForm, error messages, and login confirmation
⎪ └── LoginForm.js // renders login form fields and button
├── hooks/
⎪ └── useLogin.js // connects to API and manages state
└── index.js // stitches everything together
Option 1: Unit tests
If you’re like me, and like writing tests — perhaps with your headphones on and something good on Spotify — then you might be tempted to knock out a unit test for every file.
Even if you’re not a testing aficionado, you might be working on a project that’s “trying to be good with testing” without a clear strategy and a testing approach of “I guess each file should have its own test?”
That would look something like this (where I’ve added unit to test file names for clarity):
I went through the exercise of adding each of these unit tests on on GitHub, and created a test:coverage:unit script to generate a coverage report (a built-in feature of Jest). We can get to 100% coverage with the four unit test files:
100% coverage is usually overkill, but it’s achievable for such a simple codebase.
Let’s dig into one of the unit tests created for the onLogin React hook. Don’t worry if you’re not well-versed in React hooks or how to test them.
This test was fun to write (because React Hooks Testing Library makes testing hooks a breeze), but it has a few problems.
First, the test validates that a piece of internal state changes from 'pending' to 'resolved'; this implementation detail is not exposed to the user, and therefore, probably not a good thing to be testing. If we refactor the app, we’ll have to update this test, even if nothing changes from the user’s perspective.
Additionally, as a unit test, this is just part of the picture. If we want to validate other features of the login flow, such as the submit button text changing to “Loading,” we’ll have to do so in a different test file.
Option 2: Integration tests
Let’s consider the alternative approach of adding one integration test to validate this flow:
I implemented this test and a test:coverage:integration script to generate a coverage report. Just like the unit tests, we can get to 100% coverage, but this time it’s all in one file and requires fewer lines of code.
Here’s the integration test covering a successful login flow:
test('successful login', async () => {
// mock a successful API response
jest
.spyOn(window, 'fetch')
.mockResolvedValue({ json: () => ({ token: '123' }) });
const { getByLabelText, getByText, getByRole } = render(<LoginModule />);
const emailField = getByLabelText('Email');
const passwordField = getByLabelText('Password');
const button = getByRole('button');
// fill out and submit form
fireEvent.change(emailField, { target: { value: 'test@email.com' } });
fireEvent.change(passwordField, { target: { value: 'password' } });
fireEvent.click(button);
// it sets loading state
expect(button.disabled).toBe(true);
expect(button.textContent).toBe('Loading...');
await waitFor(() => {
// it hides form elements
expect(button).not.toBeInTheDocument();
expect(emailField).not.toBeInTheDocument();
expect(passwordField).not.toBeInTheDocument();
// it displays success text and email address
const loggedInText = getByText('Logged in as');
expect(loggedInText).toBeInTheDocument();
const emailAddressText = getByText('test@email.com');
expect(emailAddressText).toBeInTheDocument();
});
});
I really like this test, because it validates the entire login flow from the user’s perspective: the form, the loading state, and the success confirmation message. Integration tests work really well for React apps for precisely this use case; the user experience is the thing we want to test, and that almost always involves several different pieces of code working together.
This test has no specific knowledge of the components or hook that makes the expected behavior work, and that’s good. We should be able to rewrite and restructure such implementation details without breaking the tests, so long as the user experience remains the same.
I’m not going to dig into the other integration tests for the login flow’s initial state and error handling, but I encourage you to check them out on GitHub.
So, what does need a unit test?
Rather than thinking about unit vs. integration tests, let’s back up and think about how we decide what needs to be tested in the first place. LoginModule needs to be tested because it’s an entity we want consumers (other files in the app) to be able to use with confidence.
The onLogin hook, on the other hand, does not need to be tested because it’s only an implementation detail of LoginModule. If our needs change, however, and onLogin has use cases elsewhere, then we would want to add our own (unit) tests to validate its functionality as a reusable utility. (We’d also want to move the file because it wouldn’t be specific to LoginModule anymore.)
There are still plenty of use cases for unit tests, such as the need to validate reusable selectors, hooks, and plain functions. When developing your code, you might also find it helpful to practice test-driven development with a unit test, even if you later move that logic higher up to an integration test.
Additionally, unit tests do a great job of exhaustively testing against multiple inputs and use cases. For example, if my form needed to show inline validations for various scenarios (e.g. invalid email, missing password, short password), I would cover one representative case in an integration test, then dig into the specific cases in a unit test.
Other goodies
While we’re here, I want to touch on few syntactic tricks that helped my integration tests stay clear and organized.
Big waitFor Blocks
Our test needs to account for the delay between the loading and success states of LoginModule:
const button = getByRole('button');
fireEvent.click(button);
expect(button).not.toBeInTheDocument(); // too soon, the button is still there!
We can do this with DOM Testing Library’s waitFor helper:
But, what if we want to test some other items too? There aren’t a lot of good examples of how to handle this online, and in past projects, I’ve dropped additional items outside of the waitFor:
// wait for the button
await waitFor(() => {
expect(button).not.toBeInTheDocument();
});
// then test the confirmation message
const confirmationText = getByText('Logged in as test@email.com');
expect(confirmationText).toBeInTheDocument();
This works, but I don’t like it because it makes the button condition look special, even though we could just as easily switch the order of these statements:
// wait for the confirmation message
await waitFor(() => {
const confirmationText = getByText('Logged in as test@email.com');
expect(confirmationText).toBeInTheDocument();
});
// then test the button
expect(button).not.toBeInTheDocument();
It’s much better, in my opinion, to group everything related to the same update together inside the waitFor callback:
await waitFor(() => {
expect(button).not.toBeInTheDocument();
const confirmationText = getByText('Logged in as test@email.com');
expect(confirmationText).toBeInTheDocument();
});
Interestingly, an empty waitFor will also get the job done, because waitFor has a default timeout of 50ms. I find this slightly less declarative than putting your expectations inside of the waitFor, but some indentation-averse developers may prefer it:
await waitFor(() => {}); // or maybe a custom util, `await waitForRerender()`
expect(button).not.toBeInTheDocument(); // I pass!
For tests with a few steps, we can have multiple waitFor blocks in row:
const button = getByRole('button');
const emailField = getByLabelText('Email');
// fill out form
fireEvent.change(emailField, { target: { value: 'test@email.com' } });
await waitFor(() => {
// check button is enabled
expect(button.disabled).toBe(false);
});
// submit form
fireEvent.click(button);
await waitFor(() => {
// check button is no longer present
expect(button).not.toBeInTheDocument();
});
Inline it comments
Another testing best practice is to write fewer, longer tests; this allows you to correlate your test cases to significant user flows while keeping tests isolated to avoid unexpected behavior. I subscribe to this approach, but it can present challenges in keeping code organized and documenting desired behavior. We need future developers to be able to return to a test and understand what it’s doing, why it’s failing, etc.
For example, let’s say one of these expectations starts to fail:
it('handles a successful login flow', async () => {
// beginning of test hidden for clarity
expect(button.disabled).toBe(true);
expect(button.textContent).toBe('Loading...');
await waitFor(() => {
expect(button).not.toBeInTheDocument();
expect(emailField).not.toBeInTheDocument();
expect(passwordField).not.toBeInTheDocument();
const confirmationText = getByText('Logged in as test@email.com');
expect(confirmationText).toBeInTheDocument();
});
});
A developer looking into this can’t easily determine what is being tested and might have trouble deciding whether the failure is a bug (meaning we should fix the code) or a change in behavior (meaning we should fix the test).
My favorite solution to this problem is using the lesser-known test syntax for each test, and adding inline it-style comments describing each key behavior being tested:
test('successful login', async () => {
// beginning of test hidden for clarity
// it sets loading state
expect(button.disabled).toBe(true);
expect(button.textContent).toBe('Loading...');
await waitFor(() => {
// it hides form elements
expect(button).not.toBeInTheDocument();
expect(emailField).not.toBeInTheDocument();
expect(passwordField).not.toBeInTheDocument();
// it displays success text and email address
const confirmationText = getByText('Logged in as test@email.com');
expect(confirmationText).toBeInTheDocument();
});
});
These comments don’t magically integrate with Jest, so if you get a failure, the failing test name will correspond to the argument you passed to your test tag, in this case 'successful login'. However, Jest’s error messages contain surrounding code, so these it comments still help identify the failing behavior. Here’s the error message I got when I removed the not from one of my expectations:
For even more explicit errors, there’s package called jest-expect-message that allows you to define error messages for each expectation:
expect(button, 'button is still in document').not.toBeInTheDocument();
Some developers prefer this approach, but I find it a little too granular in most situations, since a single it often involves multiple expectations.
Next steps for teams
Sometimes I wish we could make linter rules for humans. If so, we could set up a prefer-integration-tests rule for our teams and call it a day.
But alas, we need to find a more analog solution to encourage developers to opt for integration tests in a situation, like the LoginModule example we covered earlier. Like most things, this comes down to discussing your testing strategy as a team, agreeing on something that makes sense for the project, and — hopefully — documenting it in an ADR.
When coming up with a testing plan, we should avoid a culture that pressures developers to write a test for every file. Developers need to feel empowered to make smart testing decisions, without worrying that they’re “not testing enough.” Jest’s coverage reports can help with this by providing a sanity check that you’re achieving good coverage, even if the tests are consolidated that the integration level.
I still don’t consider myself an expert on integration tests, but going through this exercise helped me break down a use case where integration testing delivered greater value than unit testing. I hope that sharing this with your team, or going through a similar exercise on your codebase, will help guide you in incorporating integration tests into your workflow.
The concept of an “incremental build” is that, when using some kind of generator that builds all the files that make for a website, rather than rebuilding 100% of those files every single time, it only changes the files that need to be changed since the last build. Seems like an obviously good idea, but in practice I’m sure it’s extremely tricky. How do you know what exactly which files will change and which won’t before building?
I don’t have the answer to that, but Gatsby has it figured out. Faster local builds is half the joy, the other half is that deployment also becomes faster, as the files that need to move around are far fewer.
I’d say incremental builds are a pretty damn big deal. I like seeing these hurdles get cleared Jamstack-land. I’m linking to the Netlify blog post here as getting it going on Netlify requires you to enable their “build plugins” feature which is also a real ahead-of-the-game feature, allowing you to run code during different parts of CI/CD with a really clean syntax.
Hey hey, these “chronicle” posts are little roundups of news that I haven’t gotten a chance to link up yet. They are often things that I’ve done off-site, like be a guest on a podcast or online conference. Or it’s news from other projects I work on. Or some other thing I’ve been meaning to shout out. Stuff like that! Enjoy the links!
I had a chance to chat with Paul about his Tito service about last year on ShopTalk in a really great episode. Tito is a best-in-class software tool for running a conference. It helps you build a site, sell tickets, manage attendees, run reports, and all that. Clearly the COVID-19 situation has impacted that business a lot, so I admire the accelerated pivot they are doing by creating Vito, a new platform for running online conferences, and running these conferences super quickly as a way to showcase it. If you’re running an online conference, I’d get on that invite list ASAP.
Jina Anne has been doing something new as well in the online event space. She’s been doing these 30-minute AMA (Ask Me Anything) sessions with interesting folks (excluding me). Upcoming events are here. They are five bucks, and that gets you live access and the ability to actually ask a question. Jina publishes past events to YouTube. Here’s one with me:
What do you think are some of the best habits or routines that you’ve developed over the years to help you achieve success in your life?
I’m quite sure I have more bad habits than good, so take all this with a bucket of salt. But one thing I like to do is to try to make as much of the time I spend working is spent working on something of lasting value.
That’s why I like to blog, for example. If I finish a blog post, that’s going to be published at a URL and that URL is going to get some traffic now, and at least a little bit of traffic forever. The more I do that the more I build out my base of lasting content that will serve me forever.
Over at CodePen, we’ve been busier than ever working toward our grand vision of what CodePen can become. We have a ton of focus on things lately, despite this terrible pandemic. It’s nice to be able to stay heads down into work you find important and meaningful in the best of times, and if that can be a mental escape as well, well, I’ll take it.
We’ve been building more community-showcasing features. On our Following page there are no less than three new features: (1) A “Recent” feed¹, (2) a “Top” feed, and (3) Follow suggestions. The Following page should be about 20× more interesting by my calculation! For example, the recent feed is the activity of all the people you follow, surfacing things you likely won’t want to miss.
You can toggle that feed from “Recent” over to “Top.” While that seems like a minor change, it’s actually an entirely different feed that we create that is like a ranked popularity feed, only scoped to people you follow.
Below that is a list of other recommended CodePen folks to follow that’s created just for you. I can testify that CodePen is a lot more fun when you follow people that create things you like, and that’s a fact we’re going to keep making more and more true.
We’re always pushing out little stuff, but while I’m focusing on big new things, the biggest is the fact that we’ve taken some steps toward “Custom Editors.” That is, Pen Editors that can do things that our normal Pen Editor can’t do. We’ve released two: Flutter and Vue Single File Components.
The word “feed” is new. We don’t actually use that term on the site. It’s a word we use internally on the team and what’s used by the technology we’re using. But I think it’s a good general description for the CodePen community as well, since CodePen is a developer-facing site anyway. I suppose “stream” is also a good descriptor (and just so happens to be the literal name of the tech we’re using.
This is about the time of year I would normally be telling you about the Smashing Conference I went to and the wonderful time I had there, but those in-person conferences have, of course, been re-scheduled for later in the year. At the moment, I’m still planning on Austin in October and San Francisco in November, but of course, nobody knows what the world will be like then. One thing is for sure though: online workshops. Smashing has been doing lots of these, and many of them are super deep courses that take place over several weeks.
Lots of conferences are going online and that’s kinda cool to see. It widens the possibility that anyone in the world can join, which is the web at its best. Conferences like All Day Hey are coming up in a few weeks (and is only a handful of bucks). Jamstack Conf is going virtual in May. My closest-to-home conference this year, CascadiaJS, is going virtual in September.
I got to be on the podcast Coding Zeal. I can’t figure out how to embed a BuzzSprout episode, so here’s a link.