Page speed has always been a crucial part of SEO work, and as more companies make the shift to online operations, optimization becomes more important than ever. However, it's a complex subject that tends to be very technical. What are the most crucial things to understand about your site's page speed, and how can you begin to improve? To help you answer these questions, we're sharing this popular episode of Whiteboard Friday (originally published in February 2019) where Britney Muller goes over what you need to know to get started.
Click on the whiteboard image above to open a high resolution version in a new tab!
Video Transcription
Hey, Moz fans. Welcome to another edition of Whiteboard Friday. Today we're going over all things page speed and really getting to the bottom of why it's so important for you to be thinking about and working on as you do your work.
At the very fundamental level I'm going to briefly explain just how a web page is loaded. That way we can sort of wrap our heads around why all this matters.
How a webpage is loaded
A user goes to a browser, puts in your website, and there is a DNS request. This points at your domain name provider, so maybe GoDaddy, and this points to your server where your files are located, and this is where it gets interesting. So the DOM starts to load all of your HTML, your CSS, and your JavaScript. But very rarely does this one pull all of the needed scripts or needed code to render or load a web page.
Typically the DOM will need to request additional resources from your server to make everything happen, and this is where things start to really slow down your site. Having that sort of background knowledge I hope will help in us being able to triage some of these issues.
Issues that could be slowing down your site
What are some of the most common culprits?
First and foremost is images. Large images are the biggest culprit of slow loading web pages.
Hosting can cause issues.
Plugins, apps, and widgets, basically any third-party script as well can slow down load time.
Your theme and any large files beyond that can really slow things down as well.
Redirects, the number of hops needed to get to a web page will slow things down.
Then JavaScript, which we'll get into in a second.
But all of these things can be a culprit. So we're going to go over some resources, some of the metrics and what they mean, and then what are some of the ways that you can improve your page speed today.
Page speed tools and resources
The primary resources I have listed here are Google tools and Google suggested insights. I think what's really interesting about these is we get to see what their concerns are as far as page speed goes and really start to see the shift towards the user. We should be thinking about that anyway. But first and foremost, how is this affecting people that come to your site, and then secondly, how can we also get the dual benefit of Google perceiving it as higher quality?
We know that Google suggests a website to load anywhere between two to three seconds. The faster the better, obviously. But that's sort of where the range is. I also highly suggest you take a competitive view of that. Put your competitors into some of these tools and benchmark your speed goals against what's competitive in your industry. I think that's a cool way to kind of go into this.
Chrome User Experience Report
This is Chrome real user metrics. Unfortunately, it's only available for larger, popular websites, but you get some really good data out of it. It's housed on BigQuery*, so some basic SQL knowledge is needed.
*Editor's note: We've edited this transcript for accuracy. In the video Britney said "BigML," but intended to say BigQuery. It's hard filming an advanced-topic Whiteboard Friday in a single take! :-)
Lighthouse
Lighthouse, one of my favorites, is available right in Chrome Dev Tools. If you are on a web page and you click Inspect Element and you open up Chrome Dev Tools, to the far right tab where it says Audit, you can run a Lighthouse report right in your browser.
What I love about it is it gives you very specific examples and fixes that you can do. A fun fact to know is it will automatically be on the simulated fast 3G, and notice they're focused on mobile users on 3G. I like to switch that to applied fast 3G, because it has Lighthouse do an actual run of that load. It takes a little bit longer, but it seems to be a little bit more accurate. Good to know.
Page Speed Insights
Page Speed Insights is really interesting. They've now incorporated Chrome User Experience Report. But if you're not one of those large sites, it's not even going to measure your actual page speed. It's going to look at how your site is configured and provide feedback according to that and score it. Just something good to be aware of. It still provides good value.
Test your mobile website speed and performance
I don't know what the title of this is. If you do, please comment down below. But it's located on testmysite.thinkwithgoogle.com. This one is really cool because it tests the mobile speed of your site. If you scroll down, it directly ties it into ROI for your business or your website. We see Google leveraging real-world metrics, tying it back to what's the percentage of people you're losing because your site is this slow. It's a brilliant way to sort of get us all on board and fighting for some of these improvements.
Pingdom and GTmetrix are non-Google products or non-Google tools, but super helpful as well.
Site speed metrics
So what are some of the metrics?
What is first paint?
First paint is he first non-blank paint on a screen. It could be just the first pixel change. That initial change is considered first paint.
What is first contentful paint?
First contentful paint is when the first content appears. This might be part of the nav or the search bar or whatever it might be. --That's the first contentful paint.
What is first meaningful paint?
First meaningful paint is when primary content is visible. When you sort of get that reaction of, "Oh, yeah, this is what I came to this page for," that's first meaningful paint.
What is time to interactive?
Time to interactive is when it's visually usable and engage-able. So we've all gone to a web page and it looks like it's done, but we can't quite use it yet. That's where this metric comes in. So when is it usable for the user? Again, notice how user-centric even these metrics are. Really, really neat.
DOM content loaded
The DOM content loaded, this is when the HTML is completely loaded and parsed. So some really good ones to keep an eye on and just to be aware of in general.
Ways to improve your page speed
HTTP/2
HTTP/2 can definitely speed things up. As to what extent, you have to sort of research that and test.
Preconnect, prefetch, preload
Preconnect, prefetch, and preload really interesting and important in speeding up a site. We see Google doing this on their SERPs. If you inspect an element, you can see Google prefetching some of the URLs so that it has it faster for you if you were to click on some of those results. You can similarly do this on your site. It helps to load and speed up that process.
Enable caching & use a content delivery network (CDN)
Caching is so, so important. Definitely do your research and make sure that's set up properly. Same with CDNs, so valuable in speeding up a site, but you want to make sure that your CDN is set up properly.
Compress images
The easiest and probably quickest way for you to speed up your site today is really just to compress those images. It's such an easy thing to do. There are all sorts of free tools available for you to compress them. Optimizilla is one. You can even use free tools on your computer, Save for Web, and compress properly.
Minify resources
You can also minify resources. So it's really good to be aware of what minification, bundling, and compression do so you can have some of these more technical conversations with developers or with anyone else working on the site.
So this is sort of a high-level overview of page speed. There's a ton more to cover, but I would love to hear your input and your questions and comments down below in the comment section.
I really appreciate you checking out this edition of Whiteboard Friday, and I will see you all again soon. Thanks so much. See you.
Scoop up more SEO insights at MozCon Virtual this July
Don't miss exclusive data, tips, workflows, and advice from Britney and our other fantastic speakers at this year's MozCon Virtual! Chock full of the SEO industry's top thought leadership, for the first time ever MozCon will be completely remote-friendly. It's like 20+ of your favorite Whiteboard Fridays on vitamins and doubled in size, plus interactive Q&A, virtual networking, and full access to the video bundle:
Sign up for The Moz Top 10, a semimonthly mailer updating you on the top ten hottest pieces of SEO news, tips, and rad links uncovered by the Moz team. Think of it as your exclusive digest of stuff you don't have time to hunt down but want to read!
from The Moz Blog https://ift.tt/38ptMXQ
via IFTTT
Puppeteer is an Node library for spinning up a copy of Chrome “headlessly” (i.e. no UI) and controlling it. People use it for stuff like taking a screenshot of a website or running integration tests. You can even run it in a Lambda.
Another use case is running synthetic (i.e. not based on real-users) performance tests, like some of these new Web Core Vitals
Addy Osmani lists out a bunch of these “recipes” for measuring certain performance things in Puppeteer. These would be super useful as part of a build process alongside other tests. Did the unit tests pass? Did the integration tests pass? Did the accessibility tests pass? Did the performance metrics tests pass?
BrowserStack released a thing to measure your site and give you a performance score.
You get the tests back super quick which is cool. I can see how tools like this are good for starting conversations with teams about improving performance.
But… that number seems a little weird. They don’t exactly document how it’s calculated, but it seems to be based on stuff like Time to First Byte (TTFB) and the page load event, which aren’t particularly useful performance metrics.
It’s not bad that this tool exists or anything, but I don’t think it’s for practitioners doing performance work.
Karolina Szczur from Calibre documents some common team struggles like, for example, having a team be able to identify real issues from variability noise.
Many people from different backgrounds can view performance dashboards. Not knowing what constitutes a meaningful change that needs investigation can result in false positives, lack of trust in monitoring and cycles spent looking for reasons for performance regressions or upgrades that aren’t there.
50ms. That’s how long until any particular JavaScript task starts affecting user experience. Might as well track and (ideally) fix them.
When the browser’s main thread hits max CPU for more than 50ms, a user starts to notice that their clicks are delayed and that scrolling the page has become janky and unresponsive. Batteries drain faster. People rage click or go elsewhere.
We expect a line to break when the text on that line reaches the parent box boundaries. We see this every time we create a paragraph, just like this one. When the parent box doesn’t have enough room for the next word in a line, it breaks it and moves down to the next line and repeats that process.
Well, that’s how it works when words are separated by spaces or other whitespace. As far as CSS goes, there are five (!) properties that can possibly affect how and when a line breaks. Let’s not get into all that again though. Let’s instead look at a situation where it seems like a line is going to break but it doesn’t as an excuse to learn a little something about line breaking.
What happens in a situation where there is no way for the browser to know when it’s OK to make a break?
Let’s get ourselves into a bad situation with a “tag list” and then get ourselves out of it. Here’s our markup:
<ul>
<li>PHP</li>
<li>JavaScript</li>
<li>Rust</li>
<!-- etc. -->
</ul>
Next, we’ll apply CSS that overwrites the default list style from its default vertical orientation to horizontal by making the list items display inline.
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
display: inline;
padding-right: 5px;
}
The list looks just like we want it. I added a little bit of space between one list item and another, so it doesn’t look too crowded.
Now let’s introduce a wrapper element to the mix. That’s essentially a div around the unordered list. We can give it a class, say, .tags.
Let’s say we want to give the wrapper a fixed width of 200px. That’s where we should expect to see line breaks happen as the unordered list bumps into wrapper’s boundaries.
.tags {
width: 200px;
}
Here comes the interesting part. Many people use minifiers in their build process to reduce file sizes by getting rid of unnecessary values. Some of these values are whitespaces, which includes spaces, tabs, and line breaks (such as carriage return and line feed) characters that are use for formatting purposes but considered by minifies to be irrelevant to the final result.
If we “minify” our HTML by removing new lines, then this is what we get:
UH OH. As you can see, the list is not breaking at the 200px boundary anymore. Why? What is different now? Personally, I thought HTML didn’t care about whitespaces. What is so different about the minified version from the original markup?
The browser does actually care about whitespaces… but only sometimes. And this just so happens to be one of those instances. When the page is being parsed, the parser sees this list as one long word because, from its perspective, there are no characters that differentiate one from the other.
One might think having the padding is affecting things. But if we remove the padding from our list items we still get the same result… only with no spacing between items.
The browser sees the entire list as a single word.
We can get natural line breaks from special characters
Besides spaces, excluding non-breaking spaces ( ), there are some other characters that will force a line break, including:
After hypen (‐)
After en dash (–)
Before and after em dash (—)
After question mark (?)
Zero-width white space (U+200B or ​)
These line breaks happen at rendering time which means the browser still sees this as one long word. Adding a new list item to the tag list with any of these characters will force a line break. Let’s add “Objective-C” to the list. Its name contains a hyphen, which we can use to see how it affects things.
For better readability purpose the code code will have indentation and new line.
That’s good. Let’s look at three solutions to our non-line-breaking list along these lines.
Solution 1: Add one of the breaking characters
We can keep forcing line breaks with those breaking characters like we just did. But remember, if you are using a minifier, adding the spaces in or after the closing tag won’t guaranteed it won’t be removed, as not all minifiers work the same way.
The breaking character can also be added using the ::before and ::after pseudo-elements in CSS. What makes this solution effective is that it’s not affected by an HTML minifier because the whitespace is added when the CSS is applied.
But, before we move on, let’s talk a moment about collapsing whitespaces.
The browser collapses whitespace before and after a character that forces a line break inside inline elements. With this in mind, there’s a little trick to using ::after and the content property with whitespacing and display: inline-block. The inline-block element adds a breaking character at the end of the text. Then the content property space comes after the breaking character created by the inline-block element, which results in the space being removed at rendering time. That is, unless the white-space property is set to pre.
Solution 3: Use inline-block instead
Perhaps you have bumped into a fight with space between inline-block elements in CSS before. We can use the inline-block value on the display property to force a line break because the inline-block element already has the extra whitespace we need. This works similar to adding a zero-width space character, but the list items will have no visual separation.
Solution 4: Use flex or inline-flex
Another solution is to define the unordered list as a flex container, which allows us to use flex-flow to set the direction of the list and to make sure it to multiple lines when needed.
We can also turn to the display: inline-flex instead of inline-block solution. The idea here is that the entire flexible container display inline.
So, we started this post with a situation that might come up when using a minifier. That said, minifiers — and many libraries for that matter — are smart and will try to prevent this line-breaking issue from happening.
Sure, it’s not an extremely common situation to bump into. It’s really one of those things that can fly under the radar if we’re not paying attention but, if it does happen, at least we know there are ways to work around it.
Re-imagining MozCon hasn't been easy. I won't lie — I'll miss seeing so many of you in person, and, yes, I'll miss the magic of the big stage. We're working hard to make this year special, including leveling up our speakers for their remote sessions. I recently shared my own set-up on Twitter:
This stirred up quite a bit of interest in our set-up and equipment list, so thanks to Cheryl on our events team for filling in the blanks for me, and thanks to our amazing A/V partners at Seamless Events for helping this all come together. Also, many thanks to our speakers who gave me permission to share their photos and let you in on some of the magic behind in front of the curtain.
MozCon Virtual equipment list
Before we get to the fun part (or maybe this is the fun part for you), here's the standard equipment list our A/V team used for MozCon Virtual (some speaker set-ups may vary):
Note that some of the models/sizes linked to in [more info] may not be exact matches to our kit. While Moz doesn't endorse any of these specific products, I've personally been pleasantly surprised at how affordable and accessible decent A/V equipment has become, and quarantine is making the value proposition even stronger.
The presenter remote on my desk is not part of the kit, but is my own Logitech R400 (more info). I've had this one for almost six years, and wish I'd bought my own remote sooner. I use it even when I'm presenting at my desk or practicing on a plane (that may say more about me than about Logitech, admittedly). The LEGOs and half-finished LaCroix were not included in the speaker kit, although LEGOs factor heavily into my MozCon presentation.
Sneak-peeks with our speakers
Just for fun, here's a sneak-peek at a few of our speakers and their set-ups.
I was going to make all of the photos 16:9 like the one above, but Dana ruined that by having this amazing skylight in her loft, so all of the speakers are getting big photos now.
Dana's MozCon Virtual session:
"Red Flags: Use a discovery process to go from red flags to green lights"
Shannon braved a trek to the office just for MozCon and wins the award for looking more professional than the rest of us. I cleaned my home office. That counts for something, right?
Shannon's MozCon Virtual Session:
"Great Expectations: The Truth About Digital PR Campaigns"
Robin went for the rare standing set-up. Robin has also delegated his copy of "Pandemic" to being a monitor stand, as it's far too depressing to play right now.
Robin's MozCon Virtual Session:
"Whatever You Do, Put Billboards in Seattle – Getting Brand Awareness Data from Google"
I'm not sure if Rob is expertly offsetting his window light with two ring lights or if we just forgot to send him the instruction sheet. He's my boss, so I'll assume the former.
Rob's MozCon Virtual Session:
"A Novel Approach to Scraping Websites"
Last, but certainly not least, our own CEO, Sarah Bird, who apparently gets to have a hammock outside her office, because she's the boss.
Sarah's MozCon Virtual Session:
"Welcome to MozCon Virtual 2020 + the State of the Industry"
The idea for this post was a little last-minute, and I didn't want to personally annoy every speaker with photo requests, so a big thanks to all of our speakers for going the extra mile to make the shift to a virtual event with us and set up all of this equipment. Special thanks to Cheryl and Carly for all of their work pulling this plan together.
What's your home set-up?
Have you leveled up your A/V set-up and you're just itching to show it off? Let us know about your favorite equipment in the comments, or send us your home-office photos on Twitter (@Moz).
Hope to see you at MozCon Virtual on July 14-15. No need to book a hotel or flight, so there's still time to join us, and the $129 special price includes all speaker videos!
Sign up for The Moz Top 10, a semimonthly mailer updating you on the top ten hottest pieces of SEO news, tips, and rad links uncovered by the Moz team. Think of it as your exclusive digest of stuff you don't have time to hunt down but want to read!
from The Moz Blog https://ift.tt/2VDhw0R
via IFTTT
Hard-stop gradients are one of my favorite CSS tricks. Here, Marcel Moreau combines that idea with CSS grid to solve an issue that’s otherwise a pain in the butt. Say you have like a 300px right sidebar on a desktop layout with a unique background color. Easy enough. But then say you want that background color to stretch to the right edge of the browser window even though the grid itself is width-constrained. Tricker.
Dark mode has gained a lot of traction recently. Like Apple, for instance, has added dark mode to its iOS and MacOS operating systems. Windows and Google have done the same.
DuckDuckGo’s light and dark themes
Let’s get into dark mode in the context of websites. We’ll delve into different options and approaches to implementing a dark mode design and the technical considerations they entail. We’ll also touch upon some design tips along the way.
The typical scenario is that you already have a light theme for your site, and you’re interested in making a darker counterpart. Or, even if you’re starting from scratch, you’ll have both themes: light and dark. One theme should be defined as the default that users get on first visit, which is the light theme in most cases (though we can let the user’s browser make that choice for us, as we’ll see). There also should be a way to switch to the other theme (which can be done automatically, as we’ll also see) — as in, the user clicks a button and the color theme changes.
There several approaches to go about doing this:
Using a Body Class
The trick here is to swap out a class that can be a hook for changing a style anywhere on the page.
<body class="dark-theme || light-theme">
Here’s a script for a button that will toggle that class, for example:
// Select the button
const btn = document.querySelector('.btn-toggle');
// Listen for a click on the button
btn.addEventListener('click', function() {
// Then toggle (add/remove) the .dark-theme class to the body
document.body.classList.toggle('dark-theme');
})
Here’s how we can use that idea:
<body>
<button class="btn-toggle">Toggle Dark Mode</button>
<h1>Hey there! This is just a title</h2>
<p>I am just a boring text, existing here solely for the purpose of this demo</p>
<p>And I am just another one like the one above me, because two is better than having only one</p>
<a href="#">I am a link, don't click me!</a>
</body>
The general idea of this approach is to style things up as we normally would, call that our “default” mode, then create a complete set of color styles using a class set on the <body> element we can use as a “dark” mode.
Let’s say our default is a light color scheme. All of those “light” styles are written exactly the same way you normally write CSS. Given our HTML, let’s apply some global styling to the body and to links.
body {
color: #222;
background: #fff;
}
a {
color: #0033cc;
}
Good good. We have dark text (#222) and dark links (#0033cc) on a light background (#fff). Our “default” theme is off to a solid start.
Now let’s redefine those property values, this time set on a different body class:
body {
color: #222;
background: #fff;
}
a {
color: #0033cc;
}
/* Dark Mode styles */
body.dark-theme {
color: #eee;
background: #121212;
}
body.dark-theme a {
color: #809fff;
}
Dark theme styles will be descendants of the same parent class — which is .dark-theme in this example — which we’ve applied to the <body> tag.
How do we “switch” body classes to access the dark styles? We can use JavaScript! We’ll select the button class (.btn-toggle), add a listener for when it’s clicked, then add the dark theme class (.dark-theme) to the body element’s class list. That effectively overrides all of the “light” colors we set, thanks to the cascade and specificity.
Here’s the complete code working in action. Click the toggle button to toggle in and out of dark mode.
Using Separate Stylesheets
Rather than keeping all the styles together in one stylesheet, we could instead toggle between stylesheets for each theme. This assumes you have full stylesheets ready to go.
For example, a default light theme like light-theme.css:
/* light-theme.css */
body {
color: #222;
background: #fff;
}
a {
color: #0033cc;
}
Then we create styles for the dark theme and save them in a separate stylesheet we’re calling dark-theme.css.
/* dark-theme.css */
body {
color: #eee;
background: #121212;
}
body a {
color: #809fff;
}
This gives us two separate stylesheets — one for each theme — we can link up in the HTML <head> section. Let’s link up the light styles first since we’re calling those the default.
We are using a #theme-link ID that we can select with JavaScript to, again, toggle between light and dark mode. Only this time, we’re toggling files instead of classes.
// Select the button
const btn = document.querySelector(".btn-toggle");
// Select the stylesheet <link>
const theme = document.querySelector("#theme-link");
// Listen for a click on the button
btn.addEventListener("click", function() {
// If the current URL contains "ligh-theme.css"
if (theme.getAttribute("href") == "light-theme.css") {
// ... then switch it to "dark-theme.css"
theme.href = "dark-theme.css";
// Otherwise...
} else {
// ... switch it to "light-theme.css"
theme.href = "light-theme.css";
}
});
We can also leverage the power of CSS custom properties to create a dark theme! It helps us avoid having to write separate style rulesets for each theme, making it a lot faster to write styles and a lot easier to make changes to a theme if we need to.
We still might choose to swap a body class, and use that class to re-set custom properties:
// Select the button
const btn = document.querySelector(".btn-toggle");
// Listen for a click on the button
btn.addEventListener("click", function() {
// Then toggle (add/remove) the .dark-theme class to the body
document.body.classList.toggle("dark-theme");
});
First, let’s define the default light color values as custom properties on the body element:
body {
--text-color: #222;
--bkg-color: #fff;
--anchor-color: #0033cc;
}
Now we can redefine those values on a .dark-theme body class just like we did in the first method:
Here are our rulesets for the body and link elements using custom properties:
body {
color: var(--text-color);
background: var(--bkg-color);
}
a {
color: var(--anchor-color);
}
We could just as well have defined our custom properties inside the document :root. That’s totally legit and even common practice. In that case, all the default theme styles definitions would go inside :root { } and all of the dark theme properties go inside :root.dark-mode { }.
Using Server-Side Scripts
If we’re already working with a server-side language, say PHP, then we can use it instead of JavaScript. This is a great approach if you prefer working directly in the markup.
We can have the user send a GET or POST request. Then, we let our code (PHP in this case) apply the appropriate body class when the page is reloaded. I am using a GET request (URL params) for the purpose of this demonstration.
And, yes, we can swap stylesheets just like we did in the second method.
This method has an obvious downside: the page needs to be refreshed for the toggle to take place. But a server-side solution like this is useful in persisting the user’s theme choice across page reloads, as we will see later.
Which method should you choose?
The “right” method comes down to the requirements of your project. If you are doing a large project, for example, you might go with CSS properties to help wrangle a large codebase. On the other hand, if your project needs to support legacy browsers, then another approach will need to do instead.
Moreover, there’s nothing saying we can only use one method. Sometimes a combination of methods will be the most effective route. There may even be other possible methods than what we have discussed.
Dark Mode at the Operating System Level
So far, we’ve used a button to toggle between light and dark mode but we can simply let the user’s operating system do that lifting for us. For example, many operating systems let users choose between light and dark themes directly in the system settings.
The “General” settings in MacOS System Preferences
Pure CSS
Details
Fortunately, CSS has a prefers-color-scheme media query which can be used to detect user’s system color scheme preferences. It can have three possible values: no preference, light and dark. Read more about it on MDN.
@media (prefers-color-scheme: dark) {
/* Dark theme styles go here */
}
@media (prefers-color-scheme: light) {
/* Light theme styles go here */
}
To use it, we can put the dark theme styles inside the media query.
@media (prefers-color-scheme: dark) {
body {
color: #eee;
background: #121212;
}
a {
color: #809fff;
}
}
Now, if a user has enabled dark mode from the system settings, they will get the dark mode styles by default. We don’t have to resort to JavaScript or server-side scripts to decide which mode to use. Heck, we don’t even need the button anymore!
JavaScript
Details
We can turn to JavaScript to detect the user’s preferred color scheme. This is a lot like the first method we worked with, only we’re using matchedMedia() to detect the user’s preference.
There is a downside to using JavaScript: there will likely be a quick flash of the light theme as JavaScript is executed after the CSS. That could be misconstrued as a bug.
And, of course, we can swap stylesheets instead like we did in the second method. This time, we link up both stylesheets and use the media query to determine which one is applied.
Overriding OS Settings
We just looked at how to account for a user’s system-wide color scheme preferences. But what if users want to override their system preference for a site? Just because a user prefers dark mode for their OS doesn’t always mean they prefer it on a website. That’s why providing a way to manually override dark mode, despite the system settings, is a good idea.
View Code
Let’s use the CSS custom properties approach to demonstrate how to do this. The idea is to define the custom properties for both themes like we did before, wrap dark styles up in the prefers-color-scheme media query, then define a .light-theme class inside of that we can use to override the dark mode properties, should the user want to toggle between the two modes.
/* Default colors */
body {
--text-color: #222;
--bkg-color: #fff;
}
/* Dark theme colors */
body.dark-theme {
--text-color: #eee;
--bkg-color: #121212;
}
/* Styles for users who prefer dark mode at the OS level */
@media (prefers-color-scheme: dark) {
/* defaults to dark theme */
body {
--text-color: #eee;
--bkg-color: #121212;
}
/* Override dark mode with light mode styles if the user decides to swap */
body.light-theme {
--text-color: #222;
--bkg-color: #fff;
}
}
Now we can turn back to our trusty button to toggle between light and dark themes. This way, we’re respecting the OS color preference by default and allowing the user to manually switch themes.
// Listen for a click on the button
btn.addEventListener("click", function() {
// If the OS is set to dark mode...
if (prefersDarkScheme.matches) {
// ...then apply the .light-theme class to override those styles
document.body.classList.toggle("light-theme");
// Otherwise...
} else {
// ...apply the .dark-theme class to override the default light styles
document.body.classList.toggle("dark-theme");
}
});
Browser Support
The prefers-color-scheme media query feature enjoys support by major browsers, including Chrome 76+, Firefox 67+, Chrome Android 76+ and Safari 12.5+ (13+ on iOS). It doesn’t support IE and Samsung Internet Browser.
That’s a promising amount of support!. Can I Use estimates 80.85% of user coverage.
Operating systems that currently support dark mode include MacOS (Mojave or later), iOS (13.0+), Windows (10+), and Android (10+).
Storing a User’s Preference
What we’ve looked at so far definitely does what it says in the tin: swap themes based on an OS preference or a button click. This is great, but doesn’t carry over when the user either visits another page on the site or reloads the current page.
We need to save the user’s choice so that it will be applied consistently throughout the site and on subsequent visits. To do that, we can save the user’s choice to the localStorage when the theme is toggled. Cookies are also well-suited for the job.
Let’s look at both approaches.
Using localStorage
We have a script that saves the selected theme to localStorage when the toggle takes place. In other words, when the page is reloaded, the script fetches the choice from localStorage and applies it. JavaScript is often executed after CSS, so this approach is prone to a “flash of incorrect theme” (FOIT).
View Code
// Select the button
const btn = document.querySelector(".btn-toggle");
// Select the theme preference from localStorage
const currentTheme = localStorage.getItem("theme");
// If the current theme in localStorage is "dark"...
if (currentTheme == "dark") {
// ...then use the .dark-theme class
document.body.classList.add("dark-theme");
}
// Listen for a click on the button
btn.addEventListener("click", function() {
// Toggle the .dark-theme class on each click
document.body.classList.toggle("dark-theme");
// Let's say the theme is equal to light
let theme = "light";
// If the body contains the .dark-theme class...
if (document.body.classList.contains("dark-theme")) {
// ...then let's make the theme dark
theme = "dark";
}
// Then save the choice in localStorage
localStorage.setItem("theme", theme);
});
Using Cookies with PHP
To avoid FLIC, we can use a server-side script like PHP. Instead of saving the user’s theme preference in localStorage, we will create a cookie from JavaScript and save it there. But again, this may only be feasible if you’re already working with a server-side language.
View Code
// Select the button
const btn = document.querySelector(".btn-toggle");
// Listen for a click on the button
btn.addEventListener("click", function() {
// Toggle the .dark-theme class on the body
document.body.classList.toggle("dark-theme");
// Let's say the theme is equal to light
let theme = "light";
// If the body contains the .dark-theme class...
if (document.body.classList.contains("dark-theme")) {
// ...then let's make the theme dark
theme = "dark";
}
// Then save the choice in a cookie
document.cookie = "theme=" + theme;
});
We can now check for the existence of that cookie and load the appropriate theme by applying the proper class to the <body> tag.
If your website has user accounts — like a place to log in and manage profile stuff — that’s also a great place to save theme preferences. Send those to the database where user account details are stored. Then, when the user logs in, fetch the theme from the database and apply it to the page using PHP (or whatever server-side script).
There are various ways to do this. In this example, I am fetching the user’s theme preference from the database and saving it in a session variable at the time of login.
<?php
// Login action
if (!empty($_POST['login'])) {
// etc.
// If the uuser is authenticated...
if ($loginSuccess) {
// ... save their theme preference to a session variable
$_SESSION['user_theme'] = $userData['theme'];
}
}
// Pick the session variable first if it's set; otherwise pick the cookie
$themeChoice = $_SESSION['user_theme'] ?? $_COOKIE['theme'] ?? null;
$themeClass = '';
if ($themeChoice == 'dark') {
$themeClass = 'dark-theme';
}
?>
<!DOCTYPE html>
<html lang="en">
<!-- etc. -->
<body class="<?php echo $themeClass; ?>">
<!-- etc. -->
</body>
</html>
I am using PHP’s null coalesce operator (??) to decide where to pick the theme preference: from the session or from the cookie. If the user is logged in, the value of the session variable is taken instead that of the cookie. And if the user is not logged in or has logged out, the value of cookie is taken.
Handling User Agent Styles
To inform the browser UA stylesheet about the system color scheme preferences and tell it which color schemes are supported in the page, we can use the color-scheme meta tag.
For example, let’s say the page should support both “dark” and “light” themes. We can put both of them as values in the meta tag, separated by spaces. If we only want to support a “light” theme, then we only need to use “light” as the value. This is discussed in a CSSWG GitHub issue, where it was originally proposed.
<meta name="color-scheme" content="dark light">
When this meta tag is added, the browser takes the user’s color scheme preferences into consideration when rendering UA-controlled elements of the page (like a <button>). It renders colors for the root background, form controls, and spell-check features (as well as any other UA-controlled styles) based on the user’s preference.
Although themes are manually styled for the most part (which overrides the UA styles), informing the browser about the supported themes helps to avoid even the slightest chance of a potential FOIT situation. This is true for those occasions where HTML has rendered but CSS is still waiting to load.
We can also set this in CSS:
:root {
color-scheme: light dark; /* both supported */
}
At the time of writing, the color-scheme property lacks broad browser support, though Safari and Chrome both support it.
Combining all the things!
Let’s combine everything and create a working demo that:
Automatically loads a dark or light theme based on system preferences
Allows the user to manually override their system preference
Maintains the user’s preferred theme on page reloads
Using JavaScript & Local Storage
// Select the button
const btn = document.querySelector(".btn-toggle");
// Check for dark mode preference at the OS level
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
// Get the user's theme preference from local storage, if it's available
const currentTheme = localStorage.getItem("theme");
// If the user's preference in localStorage is dark...
if (currentTheme == "dark") {
// ...let's toggle the .dark-theme class on the body
document.body.classList.toggle("dark-mode");
// Otherwise, if the user's preference in localStorage is light...
} else if (currentTheme == "light") {
// ...let's toggle the .light-theme class on the body
document.body.classList.toggle("light-mode");
}
// Listen for a click on the button
btn.addEventListener("click", function() {
// If the user's OS setting is dark and matches our .dark-mode class...
if (prefersDarkScheme.matches) {
// ...then toggle the light mode class
document.body.classList.toggle("light-mode");
// ...but use .dark-mode if the .light-mode class is already on the body,
var theme = document.body.classList.contains("light-mode") ? "light" : "dark";
} else {
// Otherwise, let's do the same thing, but for .dark-mode
document.body.classList.toggle("dark-mode");
var theme = document.body.classList.contains("dark-mode") ? "dark" : "light";
}
// Finally, let's save the current preference to localStorage to keep using it
localStorage.setItem("theme", theme);
});
I often hear that implementing dark mode is easier than designing one. While I’ll refrain from judgement, let’s look at some considerations for designing a dark theme.
You already know the basic task: swap lighter color values for darker ones and vice versa. But there are some UI elements and enhancements that are more nuanced and require more attention. Let’s take a look at those.
Dark Mode Images
A good rule is to decrease the brightness and contrast of images a bit so that it looks comfortable to the eyes when it’s against a dark background. A super bright image on a super dark background can be jarring and dimming the image reduces some of that heavy contrast.
The CSS filter() function is more than capable of handling this for us:
/* Apply the filter directly on the body tag */
body.dark-theme img {
filter: brightness(.8) contrast(1.2);
}
/* Or apply it via media query */
@media (prefers-color-scheme: dark) {
img {
filter: brightness(.8) contrast(1.2);
}
}
We can do the same sort of thing directly in the markup using the <picture> element to load different versions of an image:
<picture>
<!-- Use this image if the user's OS setting is light or unset -->
<source srcset="photo-light.png" media="(prefers-color-scheme: light) or (prefers-color-scheme: no-preference)">
<!-- Use this image if the user's OS setting is dark -->
<source srcset="photo-dark.png" media="(prefers-color-scheme: dark)">
</picture>
The downside here is that it requires supplying two files where we only have to deal with one when using CSS. This also doesn’t fully account for the user toggling the color theme on the site.
Dark Mode Shadows
Dark mode shadows are tricky. If we simply invert a dark shadow using light colors, then we get this funky thing with a light shadow on a dark background… and it’s not a good look.
It’s possible to use a dark shadow in dark mode, but the background color has to be “light” enough (like a dark gray) to provide enough contrast to actually see the shadow against it.
🔥 When implementing dark mode, don’t throw away the visual cues in the light version by naively inverting the color scheme.
Close elements should still be lighter and distant elements should still be darker – even in a dark UI. pic.twitter.com/RNxgIppDmn
Use opacity to convey depth, with high opacity regions having a lower depth. That’s to say, elements that have a higher elevation should have a lower opacity than elements that are “closer” in depth to the background.
Different shades of color create different perceptions of “depth”
Dark Mode Typography
The trick here is a lot like images: we’ve gotta balance the contrast. Use too heavy of a font and we get blaring text that’s makes us want to move away from the screen. Use too light of a font and we’ll strain our eyes while inching toward the screen to get a closer look.
The balance is somewhere in the middle. Robin has a nice write-up where he suggests a small bit of CSS that makes a big difference in legibility.
Dark Mode Icons
Icons fall into this “tricky” category because they’re sort of a cross between text and images. If we’re working with SVG icons, though, we can change the fill with CSS. On the other hand, if we’re using font icons, we can simply change the color property instead.
/* SVG icon */
body.dark-theme svg.icon path {
fill: #efefef;
}
/* Font icon (using Font Awesome as an example) */
body.dark-theme .fa {
color: #efefef;
}
A lot of the same design considerations that are true for text, are also generally applicable to icons. For example, we ought to avoid using full white and heavy outlines.
Dark Mode Colors
Pure white text on a pure black background will look jarring. The trick here is to use an off-white for the text and off-black for the background. Material Design Guidelines for example recommends #121212 for the background.
Dark Mode Color Palettes
We’ve seen the difference using off-white and off-black colors makes for text and images. Let’s expand on that a bit with tips on how to develop a full color palette.
Most things boil down to one thing: contrast. That’s why the first tip before settling on any color is to run ideas through a contrast checker to ensure color ratios conform to WCAG’s guidelines for at least a AA rating, which is a contrast ratio of 4.5:1.
That means desaturated colors are our friends when working with a dark mode design. They help prevent overbearingly bright images and still give us plenty of room to create an effective contrast ratio.
Next, remember that accent colors are meant to be enhancements. They’re likely brighter than the dark theme background color, so using them like a primary color or the background color of a large container is just as jarring and hard on the eyes as a bright image or heavy white text.
If contrast is the balance we’re trying to strike, then remember that dark mode is more than blacks and grays. What about dark blue background with pale yellow text? Or dark brown with tan? There’s an entire (and growing) spectrum of color out there and we can leverage any part of it to fuel creativity.
A few examples of colors that are dark without resorting to full-on black:
YouTube uses the CSS variables technique. They’ve defined all their colors in variables under the html selector while dark mode colors are defined under html:not(.style-scope)[dark]. When dark mode is enabled, YouTube adds a dark="true" attribute to the <html> tag. This is what they use to override the variables defined in the HTML.
YouTube adds dark=true attribute to the <html> when it switches to the dark mode.
In the wild, the CSS custom properties approach seems to be most popular. It’s being used by Dropbox Paper, Slack, and Facebook.
Simplenote uses the class-swapping method where all light style rules are descendants of a .theme-light parent class and all the dark styles fall under a .theme-dark class. When the theme is toggled, the appropriate class is applied to the <body> tag.
Simplenote uses two classes: .light-theme and .dark-theme to style the themes.
Twitter goes the extra mile and offers several themes to choose from: “Default,” “Dim,” and “Lights out.” The “Dim” option employs dark blue for a background color. Compare that to “Lights out” which uses a stark black.
Twitter offers three themes to choose from.
Dark mode or no dark mode? That is the question.
There are perfectly valid reasons on both sides. Some of those reasons even go beyond the scope of user experience and include things like timing, budget and resources.
While being considerate of why you might not want to implement a dark mode, here are reasons why you might want to have one:
It’s cool and trendy (although that’s not a reason alone to do it)
It enhances accessibility by supporting users who are sensitive to eye strain in starkly bright themes.
It allows users to decide the most comfortable way to consume content while providing us a way to maintain control over the look and feel of things. Remember, we want to beat the Reader Mode button!
It helps to preserve battery life for devices with OLED screen where brighter colors consume more energy.
It’s extremely popular and appears to be going nowhere. It’s possible that your users who prefer a dark mode (like me!) will expect your site to have one. Might as well be ready for it.