<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Google Web Dev</title>
    <description>The latest articles on DEV Community by Google Web Dev (@chromiumdev).</description>
    <link>https://dev.to/chromiumdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F290%2F2348390a-832e-4be4-b4a6-64199fc733bb.png</url>
      <title>DEV Community: Google Web Dev</title>
      <link>https://dev.to/chromiumdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chromiumdev"/>
    <language>en</language>
    <item>
      <title>What’s new in Web Payments</title>
      <dc:creator>Eiji Kitamura</dc:creator>
      <pubDate>Fri, 07 Aug 2020 05:42:12 +0000</pubDate>
      <link>https://dev.to/chromiumdev/what-s-new-in-web-payments-35ii</link>
      <guid>https://dev.to/chromiumdev/what-s-new-in-web-payments-35ii</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jJeiFO8R--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/1%2A4LWtsGNZ1GeGL09462_1Xg.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jJeiFO8R--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/1%2A4LWtsGNZ1GeGL09462_1Xg.jpeg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s been a while since my previous post, but Web Payments team at Google including myself have been commited to work on improving it. Some may wonder what’s giong on with it, so let me share my talk at an online event called “web.dev LIVE” which Google’s Web DevRel team organized early July 2020.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/ZXmKKV7R72c"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;It’s a short video, but it contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recap of Web Payments&lt;/li&gt;
&lt;li&gt;How it works&lt;/li&gt;
&lt;li&gt;Status update&lt;/li&gt;
&lt;li&gt;Upcoming features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, we’ve published a set of documentation that explains how to build a native and a web-based payment app in detail.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://web.dev/payments/"&gt;web.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Integrate Web Payments into your payment app&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/empowering-payment-apps-with-web-payments/"&gt;Empowering payment apps with Web Payments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/life-of-a-payment-transaction/"&gt;Life of a payment transaction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/setting-up-a-payment-method/"&gt;Setting up a payment method&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build a native payment app&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/android-payment-apps-developers-guide/"&gt;Android payment app developers guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/android-payment-apps-delegation/"&gt;Providing shipping and contact information from an Android payment app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build a web-based payment app&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/web-based-payment-apps-overview/"&gt;Web-based payment apps overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/registering-a-web-based-payment-app/"&gt;Registering a web-based payment app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…And more articles are coming!&lt;/p&gt;

&lt;p&gt;If you are interested in building a Web Payments compatible payment app, let me know.&lt;/p&gt;

</description>
      <category>webpayments</category>
      <category>webdevlive</category>
    </item>
    <item>
      <title>Announcing web.dev LIVE: A three day digital event</title>
      <dc:creator>Michael Solati</dc:creator>
      <pubDate>Wed, 27 May 2020 03:18:35 +0000</pubDate>
      <link>https://dev.to/chromiumdev/announcing-web-dev-live-a-three-day-digital-event-2640</link>
      <guid>https://dev.to/chromiumdev/announcing-web-dev-live-a-three-day-digital-event-2640</guid>
      <description>&lt;p&gt;Two weeks ago we, at Google, would have hosted our annual developer conference, Google I/O. Out of concern for the health and safety of all participants, and in compliance with local "shelter in place" orders, we decided to not host I/O in any capacity.&lt;/p&gt;

&lt;p&gt;To bring together the web community in this unprecedented time, Google's Web Platform team is putting together a 3-day digital event. Watch our 10-20 minute sessions for 3 hours every day, where we'll host talks about modern web development and answer your questions!&lt;/p&gt;

&lt;p&gt;From the comfort of your own home join other web developers to, &lt;em&gt;"celebrate our community's actions, learn modern web techniques and connect with each other."&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Watch live on:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;June 30th, 9am - 12pm PDT (For developers in the Americas)&lt;/li&gt;
&lt;li&gt;July 1st, 12pm - 3pm GMT (For developers in Europe &amp;amp; Africa)&lt;/li&gt;
&lt;li&gt;July 2nd, 1pm - 4pm IST (For developers in Asia &amp;amp; Australia)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be sure to sign up and get more information at &lt;a href="https://web.dev/live/" rel="noopener noreferrer"&gt;web.dev/live&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>news</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Performance auditing an eCommerce site</title>
      <dc:creator>Jeff Posnick</dc:creator>
      <pubDate>Mon, 06 Apr 2020 15:00:00 +0000</pubDate>
      <link>https://dev.to/chromiumdev/performance-auditing-an-ecommerce-site-3m8a</link>
      <guid>https://dev.to/chromiumdev/performance-auditing-an-ecommerce-site-3m8a</guid>
      <description>&lt;h2&gt;
  
  
  Alan's weekly eCommerce livestreams
&lt;/h2&gt;

&lt;p&gt;My teammate &lt;a href="https://alankent.me/"&gt;Alan&lt;/a&gt;'s been running a weekly livestream on &lt;a href="https://www.youtube.com/channel/UCyQwDaXnT7wMBBqIaAfmY7g"&gt;his YouTube channel&lt;/a&gt;, focusing on topics of interest to eCommerce websites. He asked me to come on this week and perform a site performance audit.&lt;/p&gt;

&lt;p&gt;Since I'm currently homebound due to Coronavirus concerns in NYC, I haven't had the chance to perform any &lt;a href="https://dev.to/jeffposnick/cds-perf-review-clinic-takeaways-2267"&gt;in-person site audits&lt;/a&gt; in a while. Going through the process via a livestream was a great opportunity!&lt;/p&gt;

&lt;h2&gt;
  
  
  The site review
&lt;/h2&gt;

&lt;p&gt;The review process took about 50 minutes (I'd recommend watching at 2x playback speed...) and covered some relevant findings from WebPageTest.org and Lighthouse.&lt;/p&gt;

&lt;p&gt;We choose the site, &lt;a href="https://threddies.com/"&gt;https://threddies.com/&lt;/a&gt;, "at random" based on folks who &lt;a href="https://twitter.com/akent99/status/1245466347502333952"&gt;volunteered&lt;/a&gt;. I think it ended up being fairly representative of the types of issues a lot of sites encounter.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/l4bIT3CXCpc"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Lightly edited from the notes I took during the review.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools used:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://webpagetest.org/easy"&gt;"Easy" mode on WebPageTest&lt;/a&gt; (&lt;a href="https://webpagetest.org/result/200402_FD_f791a96a04046aaa9d7583914ba1c952/"&gt;site results&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/web/tools/lighthouse"&gt;Lighthouse&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Observations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;11 seconds for main content to load — what's loaded beforehand?&lt;/li&gt;
&lt;li&gt;How many of the widgets/analytics tools can be &lt;a href="https://web.dev/native-lazy-loading/"&gt;lazily-loaded&lt;/a&gt; (or potentially removed, if appropriate)?&lt;/li&gt;
&lt;li&gt;HTTP cache expiration &lt;a href="https://web.dev/reliable/"&gt;best practices&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Use the &lt;a href="https://developers.google.com/web/tools/chrome-devtools/coverage"&gt;Coverage panel&lt;/a&gt; in Chrome Dev Tools to evaluate how much of your JS/CSS in the critical request path is being used&lt;/li&gt;
&lt;li&gt;Q: Can they get away with loading Google Maps static images instead of Google Maps widget? Answer: &lt;a href="https://developers.google.com/maps/documentation/maps-static/intro"&gt;yes, they can&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Always click on the "Learn More" link in Lighthouse for additional context!&lt;/li&gt;
&lt;li&gt;Zach Leatherman's &lt;a href="https://www.zachleat.com/web/comprehensive-webfonts/"&gt;guides to loading web fonts&lt;/a&gt; are great.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
    </item>
    <item>
      <title>YEStifications: Exploring how users engage with notification prompts in the Chrome UX Report</title>
      <dc:creator>Rick Viscomi</dc:creator>
      <pubDate>Tue, 11 Feb 2020 23:02:58 +0000</pubDate>
      <link>https://dev.to/chromiumdev/yestifications-exploring-how-users-engage-with-notification-prompts-in-the-chrome-ux-report-4h7c</link>
      <guid>https://dev.to/chromiumdev/yestifications-exploring-how-users-engage-with-notification-prompts-in-the-chrome-ux-report-4h7c</guid>
      <description>&lt;h1&gt;
  
  
  📢 example.com wants to show notifications
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API"&gt;Notifications&lt;/a&gt; can be an effective way to keep users engaged, but many websites are unable to get users past the permission prompt. A common problem is prompting out of context, as soon as the page loads or without an explanation of how they will be used. So how do users tend to engage with these permission prompts? CrUX to the rescue.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developers.google.com/web/tools/chrome-user-experience-report/"&gt;Chrome UX Report&lt;/a&gt; (CrUX) now includes data from 30,000 websites on &lt;a href="https://developers.google.com/web/updates/2020/02/notification-permission-data-in-crux"&gt;users' engagement with notification prompts&lt;/a&gt;. The data is made available in a &lt;a href="https://console.cloud.google.com/bigquery?p=chrome-ux-report&amp;amp;d=all&amp;amp;t=202001&amp;amp;page=table"&gt;public BigQuery repository&lt;/a&gt;, which we'll explore in this post to see how users engage with prompts across the web. Let's dive in!&lt;/p&gt;

&lt;h1&gt;
  
  
  The state of notification permissions
&lt;/h1&gt;

&lt;p&gt;For starters, let's get a feel for the number of websites we're working with in this dataset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="nv"&gt;`chrome-ux-report.all.202001`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results show that there are 31,515 origins with permission data. An origin is the top-level address of a website, everything before the first "/" of the path, like &lt;code&gt;https://www.example.com&lt;/code&gt;. User experience data from pages on the same website are all rolled up at the origin level.&lt;/p&gt;

&lt;p&gt;Now let's look at the permission data for a single website, for example Slack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`ignore`&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="nv"&gt;`chrome-ux-report.all.202001`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://app.slack.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Accept&lt;/th&gt;
&lt;th&gt;Dismiss&lt;/th&gt;
&lt;th&gt;Deny&lt;/th&gt;
&lt;th&gt;Ignore&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;92.10%&lt;/td&gt;
&lt;td&gt;5.67%&lt;/td&gt;
&lt;td&gt;1.96%&lt;/td&gt;
&lt;td&gt;0.28%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cR7rfPjb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/8dj9pqf09gnrry6mt8ws.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cR7rfPjb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/8dj9pqf09gnrry6mt8ws.png" alt="Pie chart of the permission rates for app.slack.com"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the Slack app has a very good permission acceptance rate for their notification prompts at 92%. In other words, 92% of users who get the prompt end up granting Slack permission to show them notifications. About 6% of users close the prompt without explicitly deciding one way or the other, 2% of users explicitly disallow Slack from showing them notifications, and ~0% of users leave the prompt entirely untouched.&lt;/p&gt;

&lt;p&gt;But how representative is Slack of the experience across the web? Let's zoom out and see how all 30,000 websites stack up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`ignore`&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="nv"&gt;`chrome-ux-report.all.202001`&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="n"&gt;origin&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="nv"&gt;`ignore`&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;accept&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--uj6cO8vV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/b548fvocysh7n5ftewdo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uj6cO8vV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/b548fvocysh7n5ftewdo.png" alt="Graph of 30,000 websites' user engagement with notification permission prompts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What are we looking at here? The curved gray area on the top right of the chart shows the distribution of ignores across all websites with permission data. Next, the red area taking up the most space in the middle of the chart represents the proportion of user engagements that reject the permission prompt. The green area that curves down from the left edge of the chart represents the proportion of user engagements that accept the prompt. And the remaining yellow area between accept and deny is the dismissal rate.&lt;/p&gt;

&lt;p&gt;We can draw a couple of insights from this visualization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Most glaringly, there's a lot of red and not a lot of green in this chart. Users are highly likely to be denying notification permission prompts rather than accepting them. Slack is an extreme example of an outlier website with a high acceptance rate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There seems to be an inverse correlation between deny and ignore rates. As sites have smaller deny rates, their ignore rates increase. We could interpret the lack of prompt interaction as an implicit rejection of permission.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if Slack is such an outlier, what does a typical website look like?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dismiss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deny&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`ignore`&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`ignore`&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`ignore`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deny&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ignore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;
    &lt;span class="nv"&gt;`chrome-ux-report.all.202001`&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Accept&lt;/th&gt;
&lt;th&gt;Dismiss&lt;/th&gt;
&lt;th&gt;Deny&lt;/th&gt;
&lt;th&gt;Ignore&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16.71%&lt;/td&gt;
&lt;td&gt;23.84%&lt;/td&gt;
&lt;td&gt;40.74%&lt;/td&gt;
&lt;td&gt;18.71%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_0F__dZM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/74on2cs7fp9ecbwve0x4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_0F__dZM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/74on2cs7fp9ecbwve0x4.png" alt="Pie chart showing the average website's notification permission response rate"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a more realistic view of the state of notification permissions. Keep in mind that we're averaging the UX for many websites together, even if they have drastically different numbers of users. So this is showing us that the average website has a notification prompt acceptance rate of 17%, 24% dismissal, 41% rejection, and 19% ignore.&lt;/p&gt;

&lt;p&gt;What else can we learn about user behavior from this dataset? We could also slice this average distribution by users' device form factor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dismiss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deny&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`ignore`&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`ignore`&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;form_factor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`ignore`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deny&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ignore&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dismiss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;
    &lt;span class="nv"&gt;`chrome-ux-report.all.202001`&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="n"&gt;device&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="n"&gt;accept&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device&lt;/th&gt;
&lt;th&gt;Accept&lt;/th&gt;
&lt;th&gt;Dismiss&lt;/th&gt;
&lt;th&gt;Deny&lt;/th&gt;
&lt;th&gt;Ignore&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;phone&lt;/td&gt;
&lt;td&gt;22.76%&lt;/td&gt;
&lt;td&gt;18.03%&lt;/td&gt;
&lt;td&gt;57.33%&lt;/td&gt;
&lt;td&gt;1.88%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tablet&lt;/td&gt;
&lt;td&gt;18.75%&lt;/td&gt;
&lt;td&gt;20.24%&lt;/td&gt;
&lt;td&gt;58.34%&lt;/td&gt;
&lt;td&gt;2.67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;desktop&lt;/td&gt;
&lt;td&gt;6.11%&lt;/td&gt;
&lt;td&gt;34.01%&lt;/td&gt;
&lt;td&gt;11.61%&lt;/td&gt;
&lt;td&gt;48.27%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RIV4DF6U--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/cwq8vv3zfxnn9ezxz4e7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RIV4DF6U--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/cwq8vv3zfxnn9ezxz4e7.png" alt="Bar charts of phone, tablet, and desktop average notification permission prompt rates"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ok this is significant. There is a &lt;em&gt;huge&lt;/em&gt; difference in engagement between phones and desktops. Phone users are very unlikely to ignore the prompt, whereas desktop users ignore the prompt about half the time on average. The simplest explanation is screen size; it's a lot harder to ignore a prompt when it's taking up a larger portion of your screen. Interestingly, despite the 57% rejection rate, phone users are the most likely to accept permission prompts, at 23%, compared to just 6% on desktop. One explanation may be that a phone is a much more natural and convenient form factor on which to receive notifications because it goes where you go.&lt;/p&gt;

&lt;h1&gt;
  
  
  Where we go from here
&lt;/h1&gt;

&lt;p&gt;Users have been feeling the frustration of intrusive notification prompts and developers have only anecdotally heard about and experienced this frustration. Now we have actionable data to shed light on the quality of experience across the web. And it's clear from the CrUX data that relatively few websites are effectively enrolling users in notifications.&lt;/p&gt;

&lt;p&gt;The Chrome team has announced that it is rolling out &lt;a href="https://blog.chromium.org/2020/01/introducing-quieter-permission-ui-for.html"&gt;changes to how prompts are shown to users&lt;/a&gt; to minimize frustration, saying "sites with very low acceptance rates will be automatically enrolled in quieter prompts". So it's especially important for site owners to be aware of their prompt performance.&lt;/p&gt;

&lt;p&gt;If your site requests permission to show notifications, &lt;strong&gt;add instrumentation&lt;/strong&gt; so you can monitor the response rates. The CrUX dataset is not a substitute for your own UX monitoring, but it can be helpful for filling in gaps, seeing how similar websites are performing, and tracking the trends across the web. Read up on &lt;a href="https://developers.google.com/web/fundamentals/push-notifications/permission-ux"&gt;best practices&lt;/a&gt; for requesting permission to improve your response rates and get more &lt;em&gt;yestifications&lt;/em&gt; than notifications. 🥁&lt;/p&gt;

&lt;p&gt;The queries shown in this post can be run on BigQuery. You're strongly encouraged to remix the queries to discover your own insights. Read more about &lt;a href="https://web.dev/chrome-ux-report-bigquery/"&gt;using CrUX on BigQuery&lt;/a&gt; to get started. If you have any questions or feedback about the data, the CrUX team would love to hear from you on any of these channels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://groups.google.com/a/chromium.org/forum/#!forum/chrome-ux-report"&gt;chrome-ux-report&lt;/a&gt; on Google Groups&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://twitter.com/ChromeUXReport"&gt;@ChromeUXReport&lt;/a&gt; on Twitter&lt;/li&gt;
&lt;li&gt;questions tagged with &lt;a href="https://stackoverflow.com/questions/tagged/chrome-ux-report"&gt;chrome-ux-report&lt;/a&gt; on StackOverflow&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/GoogleChrome/CrUX"&gt;GoogleChrome/CrUX&lt;/a&gt; on GitHub&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>ux</category>
      <category>datascience</category>
      <category>sql</category>
    </item>
    <item>
      <title>Fixing layout instability</title>
      <dc:creator>Rick Viscomi</dc:creator>
      <pubDate>Wed, 18 Sep 2019 21:32:50 +0000</pubDate>
      <link>https://dev.to/chromiumdev/fixing-layout-instability-176c</link>
      <guid>https://dev.to/chromiumdev/fixing-layout-instability-176c</guid>
      <description>&lt;p&gt;In an earlier post I wrote about &lt;a href="https://dev.to/chromiumdev/measuring-cumulative-layout-shift-cls-in-webpagetest-5cle"&gt;measuring Cumulative Layout Shift&lt;/a&gt; (CLS) in WebPageTest. CLS is an aggregation of all layout shifts, so in this post I thought it'd be interesting to dive deeper and inspect each individual layout shift on a page to try to understand what could be causing the instability and actually try to fix the issue(s).&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring layout shifts
&lt;/h2&gt;

&lt;p&gt;Using the Layout Instability API, we can get a list of all layout shift events on a page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hadRecentInput&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;layout-shift&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces an array of layout shifts that are not preceded by input events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entryType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"layout-shift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"startTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;210.78500000294298&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0001045969445437389&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hadRecentInput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lastInputTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example there was a single very tiny shift of 0.01% at 210ms.&lt;/p&gt;

&lt;p&gt;Knowing the time and severity of the shift is useful to help narrow down what could have caused the shift. Let's turn back to &lt;a href="https://webpagetest.org" rel="noopener noreferrer"&gt;WebPageTest&lt;/a&gt; for a lab environment to do more testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring layout shifts in WebPageTest
&lt;/h2&gt;

&lt;p&gt;Similar to measuring CLS in WebPageTest, measuring individual layout shifts will require a custom metric. Fortunately, the process is easier now that Chrome 77 is stable. The Layout Instability API is enabled by default, so you should be able to execute that JS snippet on any website within Chrome 77 and get results immediately. In WebPageTest, you can use the default Chrome browser and not have to worry about command line flags or using Canary.&lt;/p&gt;

&lt;p&gt;So let's modify that script to produce a custom metric for WebPageTest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;LayoutShifts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hadRecentInput&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;layout-shift&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The promise in this script resolves to a JSON representation of the array rather than the array itself. This is because custom metrics can only produce primitive data types like strings or numbers.&lt;/p&gt;

&lt;p&gt;The website I'll use for the test is &lt;a href="https://ismyhostfastyet.com/" rel="noopener noreferrer"&gt;ismyhostfastyet.com&lt;/a&gt;, a site I built to compare real world loading performance of web hosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying causes of layout instability
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="http://webpagetest.org/custom_metrics.php?test=190918_6E_ef3c166b4a34033171d47e389cf82939&amp;amp;run=5&amp;amp;cached=0" rel="noopener noreferrer"&gt;results&lt;/a&gt; we can see the LayoutShifts custom metric has this value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entryType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"layout-shift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"startTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3087.2349999990547&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.3422101449275362&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hadRecentInput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lastInputTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To summarize, there is a single layout shift of 34.2% happening at 3087ms. To help identify the culprit, let's use WebPageTest's &lt;a href="http://webpagetest.org/video/compare.php?tests=190918_6E_ef3c166b4a34033171d47e389cf82939-r%3A5-c%3A0&amp;amp;thumbSize=200&amp;amp;ival=100&amp;amp;end=visual" rel="noopener noreferrer"&gt;filmstrip view&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Farg61baxb690dbn38pqc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Farg61baxb690dbn38pqc.png" alt="Two cells in the filmstrip, showing screenshots before and after the layout shift"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scrolling to the ~3 second mark in the filmstrip shows us exactly what the cause of the 34% layout shift is: the colorful table. The way the website is built is to asynchronously fetch a JSON file, then render it to a table. The table is initially empty, so waiting to fill it up when the results are loaded is causing the shift.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F8oabdvlzekzq5srtvoa9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F8oabdvlzekzq5srtvoa9.png" alt="Web font header appearing out of nowhere"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But that's not all. When the page is visually complete at ~4.3 seconds, we can see that the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; of the page "Is my host fast yet?" appears out of nowhere. This happens because the site uses a web font and hasn't taken any steps to optimize rendering. The layout doesn't actually appear to shift when this happens, but it's still a poor user experience to have to wait so long to read the title.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing layout instability
&lt;/h2&gt;

&lt;p&gt;Now that we know the asynchronously generated table is causing one third of the viewport to shift, it's time to fix it. We don't know the contents of the table until the JSON results are actually loaded, but we can still populate the table with some kind of &lt;em&gt;placeholder data&lt;/em&gt; so that the layout itself is relatively stable when the DOM is rendered.&lt;/p&gt;

&lt;p&gt;Here's the code to generate placeholder data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRandomFiller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;filler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;█&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filler&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRandomDistribution&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;fast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;slow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fast&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Temporary placeholder data.&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRandomDistribution&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getRandomFiller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getRandomFiller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getRandomFiller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;slow&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;updateResultsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sortResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The placeholder data is generated randomly before being sorted. It includes the "█" character repeated a random number of times to create visual placeholders for the text and a randomly generated distribution of the three main values. I also added some styles to desaturate all color from the table to make it clear that the data is not fully loaded yet.&lt;/p&gt;

&lt;p&gt;The appearance of the placeholders you use don't matter for layout stability as much as they are for assuring users that content &lt;em&gt;is&lt;/em&gt; coming and the page isn't broken.&lt;/p&gt;

&lt;p&gt;Here's what the placeholders look like while the JSON data is loading:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F4t7x6h3ibkgpmr1pxgy1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F4t7x6h3ibkgpmr1pxgy1.png" alt="The data table is rendered with placeholder data"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Addressing the web font issue is much simpler. Because the site is using Google Fonts, we just need to pass in the &lt;code&gt;display=swap&lt;/code&gt; property in the CSS request. That's all. The Fonts API will add the &lt;code&gt;display: swap&lt;/code&gt; style in the font declaration, enabling the browser to render text in a fallback font immediately. Here's the corresponding markup with the fix included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com/css?family=Chivo:900&amp;amp;display=swap"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verifying the optimizations
&lt;/h2&gt;

&lt;p&gt;After rerunning the page through WebPageTest, we can generate a before and after &lt;a href="http://webpagetest.org/video/compare.php?tests=190918_6E_ef3c166b4a34033171d47e389cf82939%2C190918_WF_60f9c9a1c669b20039860c09ca27df7c&amp;amp;thumbSize=200&amp;amp;ival=100&amp;amp;end=visual" rel="noopener noreferrer"&gt;comparison&lt;/a&gt; to visualize the difference and measure the new degree of layout instability:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fe23be8b4l6094cstdzgn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fe23be8b4l6094cstdzgn.png" alt="WebPageTest filmstrip showing both sites loading side-by-side with and without layout optimizations"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entryType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"layout-shift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"startTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3070.9349999997357&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.000050272187989256116&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hadRecentInput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lastInputTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to the &lt;a href="http://webpagetest.org/custom_metrics.php?test=190918_WF_60f9c9a1c669b20039860c09ca27df7c&amp;amp;run=9&amp;amp;cached=0" rel="noopener noreferrer"&gt;custom metric&lt;/a&gt;, there is still a layout shift occurring at 3071ms (about the same time as before) but the severity of the shift is &lt;em&gt;much&lt;/em&gt; smaller: 0.005%. I can live with this.&lt;/p&gt;

&lt;p&gt;It's also clear from the filmstrip that the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; font is immediately falling back to a system font, enabling users to read it sooner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Complex websites will probably experience many more layout shifts than in this example, but the remediation process is still the same: add layout instability metrics to WebPageTest, cross reference the results with the visual loading filmstrip to identify the culprits, and implement a fix using placeholders to reserve the screen real estate.&lt;/p&gt;

&lt;h3&gt;
  
  
  (One more thing)
&lt;/h3&gt;

&lt;p&gt;It's nice to be able to run WebPageTest on a page before and after an optimization and see an improvement to a metric, but what really matters is that the user experience is actually getting better. Isn't that why we're trying to make the site better in the first place?&lt;/p&gt;

&lt;p&gt;So what would be great is if we start measuring the layout instability experiences of real users along with our traditional web performance metrics. This is a crucial piece of the optimization feedback loop because having data from the field tells us where the problems are and whether our fixes made a positive difference.&lt;/p&gt;

&lt;p&gt;In addition to collecting your own layout instability data, check out the &lt;a href="https://twitter.com/ChromeUXReport/status/1138555303379816448" rel="noopener noreferrer"&gt;Chrome UX Report&lt;/a&gt;, which includes Cumulative Layout Shift data from real user experiences on millions of websites. It allows you to find out how you (or your competitors) are performing, or you can use it to explore the state of layout instability across the web.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webpagetest</category>
      <category>ux</category>
    </item>
    <item>
      <title>[Video] The State of the Web: Developing with a global mindset</title>
      <dc:creator>Rick Viscomi</dc:creator>
      <pubDate>Sat, 07 Sep 2019 18:55:14 +0000</pubDate>
      <link>https://dev.to/chromiumdev/video-the-state-of-the-web-developing-with-a-global-mindset-1ajo</link>
      <guid>https://dev.to/chromiumdev/video-the-state-of-the-web-developing-with-a-global-mindset-1ajo</guid>
      <description>&lt;p&gt;To many web developers, having a global mindset just means translating the content into various languages. There are also cultural differences that may make a website difficult or even impossible to use. In this video with &lt;a href="https://twitter.com/sitnikcode"&gt;Andrey Sitnik&lt;/a&gt;, he and I talk about some of the tools and techniques for inclusivity.&lt;/p&gt;

&lt;p&gt;Some highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users' names can be &lt;em&gt;anything&lt;/em&gt; and might not fit the template of your sign up form. Rather than forcing a first and last name, allow them to enter their name into a single "Name" field. And whatever you do, don't enforce minimum character limits!&lt;/li&gt;
&lt;li&gt;The browsers you support and test with can have a big effect on who is able to use your website. There are pockets of the world where some browsers are more popular than others and the distribution of popularity is not evenly spread.&lt;/li&gt;
&lt;li&gt;Having fast and reliable access to the internet can be a luxury in some parts of the world. Even in developed countries, connectivity can be intermittent, so building offline resilience lowers the barrier to use.&lt;/li&gt;
&lt;li&gt;Expand your global mindset by busting out of your tech bubble. If you have the means, attend conferences in parts of the world where you wouldn't normally go. Something everyone can do is simply follow underrepresented people on social media to be exposed to new and different ideas. When you reshare other people's content, think twice about creating an echo chamber and instead consider amplifying underrepresented voices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What other takeaways resonated with you? &lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/F2N-evGOcxc"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>Hello darkness, my old friend</title>
      <dc:creator>Thomas Steiner</dc:creator>
      <pubDate>Mon, 05 Aug 2019 14:12:46 +0000</pubDate>
      <link>https://dev.to/chromiumdev/hello-darkness-my-old-friend-3jcg</link>
      <guid>https://dev.to/chromiumdev/hello-darkness-my-old-friend-3jcg</guid>
      <description>&lt;h4&gt;
  
  
  Overhyped or necessity? Learn everything about dark mode and how to support it to the benefit of your users!
&lt;/h4&gt;

&lt;p&gt;(Originally published on &lt;a href="https://web.dev/prefers-color-scheme/"&gt;https://web.dev/prefers-color-scheme/&lt;/a&gt;.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;📚 I have done a lot of background research on the history and theory of dark mode, if you are only interested in working with dark mode, feel free to &lt;a href="https://web.dev/prefers-color-scheme/#activating-dark-mode-in-the-operating-system"&gt;skip the introduction&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Dark mode before Dark Mode
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DMQ-0ujp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/640/0%2AYZQl9JWZBeUzhdLF.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DMQ-0ujp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/640/0%2AYZQl9JWZBeUzhdLF.jpg" alt=""&gt;&lt;/a&gt;Green screen (&lt;a href="https://commons.wikimedia.org/wiki/File:Compaq_Portable_and_Wordperfect.JPG"&gt;Source&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;We have gone full circle with dark mode. In the dawn of personal computing, dark mode wasn’t a matter of choice, but a matter of fact: Monochrome CRT computer monitors worked by firing electron beams on a phosphorescent screen and the phosphor used in early CRTs was green. Because text was displayed in green and the rest of the screen was black, these models were often referred to as &lt;a href="https://commons.wikimedia.org/wiki/File:Schneider_CPC6128_with_green_monitor_GT65,_start_screen.jpg"&gt;green screens&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JynDJ8Z1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/698/0%2AILIBhfzAmuuP6fAK.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JynDJ8Z1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/698/0%2AILIBhfzAmuuP6fAK.jpg" alt=""&gt;&lt;/a&gt;Dark-on-white (&lt;a href="https://www.youtube.com/watch?v=qKkABzt0Zqg"&gt;Source&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The subsequently introduced Color CRTs displayed multiple colors through the use of red, green, and blue phosphors. They created white by activating all three phosphors simultaneously. With the advent of more sophisticated WYSIWYG &lt;a href="https://en.wikipedia.org/wiki/Desktop_publishing"&gt;desktop publishing&lt;/a&gt;, the idea of making the virtual document resemble a physical sheet of paper became popular.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EA_UDgfO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/800/0%2AQOV2uc-yZ1Tef17O.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EA_UDgfO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/800/0%2AQOV2uc-yZ1Tef17O.png" alt=""&gt;&lt;/a&gt;The WorldWideWeb browser (&lt;a href="https://commons.wikimedia.org/wiki/File:WorldWideWeb_FSF_GNU.png"&gt;Source&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This is where &lt;em&gt;dark-on-white&lt;/em&gt; as a design trend started, and this trend was carried over to the &lt;a href="http://info.cern.ch/hypertext/WWW/TheProject.html"&gt;early document-based web&lt;/a&gt;. The first ever browser, &lt;a href="https://en.wikipedia.org/wiki/WorldWideWeb"&gt;WorldWideWeb&lt;/a&gt; (remember, &lt;a href="https://en.wikipedia.org/wiki/Cascading_Style_Sheets#History"&gt;CSS wasn’t even invented&lt;/a&gt; yet), &lt;a href="https://commons.wikimedia.org/wiki/File:WorldWideWeb_FSF_GNU.png"&gt;displayed webpages&lt;/a&gt; this way. Fun fact: the second ever browser, &lt;a href="https://en.wikipedia.org/wiki/Line_Mode_Browser"&gt;Line Mode Browser&lt;/a&gt; — a terminal-based browser — was green on dark. These days, web pages and web apps are typically designed with dark text on a light background, a baseline assumption that is also hard-coded in user agent stylesheets, including &lt;a href="https://chromium.googlesource.com/chromium/blink/+/master/Source/core/css/html.css"&gt;Chrome’s&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fImk0Jui--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/500/0%2A7glgTBgIbbQOgpju.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fImk0Jui--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/500/0%2A7glgTBgIbbQOgpju.jpg" alt=""&gt;&lt;/a&gt;Smartphone used in bed (&lt;a href="https://unsplash.com/photos/W39xsPWZgA4"&gt;Source&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The days of CRTs are long over. Content consumption and creation has shifted to mobile devices that use backlit LCD or energy-saving AMOLED screens. Smaller and more transportable computers, tablets, and smartphones led to new usage patterns. Leisure tasks like web browsing, coding for fun, and high-end gaming frequently happen after-hours in dim environments. People even enjoy their devices in their beds at night-time. The more people use their devices in the dark, the more the idea of going back to the roots of &lt;em&gt;light-on-dark&lt;/em&gt; becomes popular.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why dark mode
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Dark mode for aesthetic reasons
&lt;/h4&gt;

&lt;p&gt;When people get asked &lt;a href="https://medium.com/dev-channel/let-there-be-darkness-maybe-9facd9c3023d"&gt;why they like or want dark mode&lt;/a&gt;, the most popular response is that &lt;em&gt;“it’s easier on the eyes,”_followed by _“it’s elegant and beautiful.”&lt;/em&gt; Apple in their &lt;a href="https://developer.apple.com/documentation/appkit/supporting_dark_mode_in_your_interface"&gt;Dark Mode developer documentation&lt;/a&gt; explicitly writes: &lt;em&gt;“The choice of whether to enable a light or dark appearance is an aesthetic one for most users, and might not relate to ambient lighting conditions.”&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👩‍🔬 Read up more on &lt;a href="https://medium.com/dev-channel/let-there-be-darkness-maybe-9facd9c3023d"&gt;user research regarding why people want dark mode and how they use it&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NgXd-OC_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/531/0%2AZfv5E4GlFzQp4ZLN.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NgXd-OC_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/531/0%2AZfv5E4GlFzQp4ZLN.png" alt=""&gt;&lt;/a&gt;System 7 CloseView (&lt;a href="https://archive.org/details/mac_Macintosh_System_7_at_your_Fingertips_1992"&gt;Source&lt;/a&gt;)&lt;/p&gt;

&lt;h4&gt;
  
  
  Dark mode as an accessibility tool
&lt;/h4&gt;

&lt;p&gt;There are also people who actually &lt;em&gt;need&lt;/em&gt; dark mode and use it as another accessibility tool, for example, users with low vision. The earliest occurrence of such an accessibility tool I could find is &lt;a href="https://en.wikipedia.org/wiki/System_7"&gt;System&lt;/a&gt;7’s &lt;em&gt;CloseView&lt;/em&gt; feature, which had a toggle for &lt;em&gt;Black on White&lt;/em&gt; and &lt;em&gt;White on Black&lt;/em&gt;. While System 7 supported color, the default user interface was still black-and-white.&lt;/p&gt;

&lt;p&gt;These inversion-based implementations demonstrated their weaknesses once color was introduced. User research by Szpiro &lt;em&gt;et al.&lt;/em&gt; on &lt;a href="https://dl.acm.org/citation.cfm?id=2982168"&gt;how people with low vision access computing devices&lt;/a&gt; showed that all interviewed users disliked inverted images, but that many preferred light text on a dark background. Apple accommodates for this user preference with a feature called &lt;a href="https://www.apple.com//accessibility/iphone/vision/"&gt;Smart Invert&lt;/a&gt;, which reverses the colors on the display, except for images, media, and some apps that use dark color styles.&lt;/p&gt;

&lt;p&gt;A special form of low vision is Computer Vision Syndrome, also known as Digital Eye Strain, which is &lt;a href="https://onlinelibrary.wiley.com/doi/full/10.1111/j.1475-1313.2011.00834.x"&gt;defined&lt;/a&gt; as &lt;em&gt;“the combination of eye and vision problems associated with the use of computers (including desktop, laptop, and tablets) and other electronic displays (e.g. smartphones and electronic reading devices).”&lt;/em&gt; It has been &lt;a href="https://bmjopen.bmj.com/content/5/1/e006748"&gt;proposed&lt;/a&gt; that the use of electronic devices by adolescents, particularly at night time, leads to an increased risk of shorter sleep duration, longer sleep-onset latency, and increased sleep deficiency. Additionally, exposure to blue light has been widely &lt;a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4254760/"&gt;reported&lt;/a&gt; to be involved in the regulation of &lt;a href="https://en.wikipedia.org/wiki/Circadian_rhythm"&gt;circadian rhythm&lt;/a&gt; and the sleep cycle, and irregular light environments may lead to sleep deprivation, possibly affecting mood and task performance, according to &lt;a href="https://www.college-optometrists.org/oip-resource/computer-vision-syndrome--a-k-a--digital-eye-strain.html"&gt;research by Rosenfield&lt;/a&gt;. To limit these negative effects, reducing blue light by adjusting the display color temperature through features like iOS’ &lt;a href="https://support.apple.com/en-us/HT207570"&gt;Night Shift&lt;/a&gt; or Android’s &lt;a href="https://support.google.com/pixelphone/answer/7169926"&gt;Night Light&lt;/a&gt; can help, as well as avoiding bright lights or irregular lights in general through dark themes or dark modes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Dark mode power savings on AMOLED screens
&lt;/h4&gt;

&lt;p&gt;Finally, dark mode is known to save a &lt;em&gt;lot&lt;/em&gt; of energy on AMOLED screens. Android case studies that focused on popular Google apps like YouTube have shown that the power savings can be up to 60%. The video below has more details on these case studies and the power savings per app.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/N_6sPd0Jd3g"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Activating dark mode in the operating system
&lt;/h3&gt;

&lt;p&gt;Now that I have covered the background of why dark mode is such a big deal for many users, let’s review how you can support it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0w4FiTWe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/610/0%2AErOuqrV3HKXKLA3w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0w4FiTWe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/610/0%2AErOuqrV3HKXKLA3w.png" alt=""&gt;&lt;/a&gt;Android Q dark theme settings&lt;/p&gt;

&lt;p&gt;Operating systems that support a dark mode or dark theme typically have an option to activate it somewhere in the settings. On macOS X, it’s in the system preference’s &lt;em&gt;General&lt;/em&gt; section and called &lt;em&gt;Appearance&lt;/em&gt; (&lt;a href="https://web.dev/prefers-color-scheme/macosx.png"&gt;screenshot&lt;/a&gt;), and on Windows 10, it’s in the &lt;em&gt;Colors&lt;/em&gt; section and called &lt;em&gt;Choose your color&lt;/em&gt; (&lt;a href="https://web.dev/prefers-color-scheme/windows10.png"&gt;screenshot&lt;/a&gt;). For Android Q, you can find it under &lt;em&gt;Display&lt;/em&gt; as a &lt;em&gt;Dark Theme&lt;/em&gt; toggle switch (&lt;a href="https://web.dev/prefers-color-scheme/android.png"&gt;screenshot&lt;/a&gt;), and on iOS 13, you can change the &lt;em&gt;Appearance&lt;/em&gt; in the &lt;em&gt;Display &amp;amp; Brightness&lt;/em&gt; section of the settings (&lt;a href="https://web.dev/prefers-color-scheme/ios.jpg"&gt;screenshot&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;prefers-color-scheme&lt;/code&gt; media query
&lt;/h3&gt;

&lt;p&gt;One last bit of theory before I get going. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries"&gt;Media queries&lt;/a&gt; allow authors to test and query values or features of the user agent or display device, independent of the document being rendered. They are used in the CSS @media rule to conditionally apply styles to a document, and in various other contexts and languages, such as HTML and JavaScript. &lt;a href="https://drafts.csswg.org/mediaqueries-5/"&gt;Media Queries Level 5&lt;/a&gt; introduces so-called user preference media features, that is, a way for sites to detect the user's preferred way to display content.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;☝️ An established user preference media feature is prefers-reduced-motion that lets you detect the desire for less motion on a page. I have &lt;a href="https://developers.google.com/web/updates/2019/03/prefers-reduced-motion"&gt;written about&lt;/a&gt;&lt;a href="https://developers.google.com/web/updates/2019/03/prefers-reduced-motion"&gt;prefers-reduced-motion&lt;/a&gt; before.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme"&gt;prefers-color-scheme&lt;/a&gt; media feature is used to detect if the user has requested the page to use a light or dark color theme. It works with the following values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;no-preference&lt;/code&gt;: Indicates that the user has made no preference known to the system. This keyword value evaluates as &lt;code&gt;false&lt;/code&gt; in the &lt;a href="https://drafts.csswg.org/mediaqueries-5/#boolean-context"&gt;boolean context&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;light&lt;/code&gt;: Indicates that the user has notified the system that they prefer a page that has a light theme (dark text on light background).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dark&lt;/code&gt;: Indicates that the user has notified the system that they prefer a page that has a dark theme (light text on dark background).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Supporting dark mode
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Finding out if dark mode is supported by the browser
&lt;/h3&gt;

&lt;p&gt;As dark mode is reported through a media query, you can easily check if the current browser supports dark mode by checking if the media query prefers-color-schemematches at all. Note how I don't include any value, but purely check if the media query alone matches.&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;media&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🎉 Dark mode is supported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;At the time of writing, prefers-color-scheme is supported on both desktop and mobile (where available) by Chrome and Edge as of version 76, Firefox as of version 67, and Safari as of version 12.1 on macOS and as of version 13 on iOS. For all other browsers, you can check the &lt;a href="https://caniuse.com/#feat=prefers-color-scheme"&gt;Can I use support tables&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is a custom element &lt;a href="https://github.com/GoogleChromeLabs/dark-mode-toggle"&gt;&lt;code&gt;&amp;lt;dark-mode-toggle&amp;gt;&lt;/code&gt;&lt;/a&gt;available that adds dark mode support to older browsers. I write about it &lt;a href="https://web.dev/prefers-color-scheme/#the-lessdark-mode-togglegreater-custom-element"&gt;further down in this article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Dark mode in practice
&lt;/h3&gt;

&lt;p&gt;Let’s finally see how supporting dark mode looks like in practice. Just like with the &lt;a href="https://en.wikipedia.org/wiki/Highlander_(film)"&gt;Highlander&lt;/a&gt;, with dark mode &lt;em&gt;there can be only one&lt;/em&gt;: dark or light, but never both! Why do I mention this? Because this fact should have an impact on the loading strategy. Please don’t force users to download CSS in the critical rendering path that is for a mode they don’t currently use. To optimize load speed, I have therefore split my CSS for the example app that shows the following recommendations in practice into three parts in order to &lt;a href="https://web.dev/defer-non-critical-css/"&gt;defer non-critical CSS&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;style.css&lt;/code&gt; that contains generic rules that are used universally on the site.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dark.css&lt;/code&gt; that contains only the rules needed for dark mode.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;light.css&lt;/code&gt; that contains only the rules needed for light mode.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Loading strategy
&lt;/h3&gt;

&lt;p&gt;The two latter ones, light.css and dark.css, are loaded conditionally with a &lt;code&gt;&amp;lt;link media&amp;gt;&lt;/code&gt; query. Initially, &lt;a href="https://caniuse.com/#feat=prefers-color-scheme"&gt;not all browsers will support&lt;/a&gt;&lt;a href="https://caniuse.com/#feat=prefers-color-scheme"&gt;prefers-color-scheme&lt;/a&gt;(detectable using the &lt;a href="https://web.dev/prefers-color-scheme/#finding-out-if-dark-mode-is-supported-by-the-browser"&gt;pattern above&lt;/a&gt;), which I deal with dynamically by loading the default light.css file via a conditionally inserted &lt;code&gt;&amp;lt;link rel="stylesheet"&amp;gt;&lt;/code&gt; element in a minuscule inline script (light is an arbitrary choice, I could also have made dark the default fallback experience). To avoid a &lt;a href="https://en.wikipedia.org/wiki/Flash_of_unstyled_content"&gt;flash of unstyled content&lt;/a&gt;, I hide the content of the page until light.css has loaded.&lt;/p&gt;


&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
 &lt;span class="c1"&gt;// If `prefers-color-scheme` is not supported, fall back to light mode.&lt;/span&gt;
 &lt;span class="c1"&gt;// In this case, light.css will be downloaded with `highest` priority.&lt;/span&gt;
 &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;media&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insertAdjacentHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beforeend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;link rel="stylesheet" href="/light.css" onload="document.documentElement.style.display = ``"&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
 &lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!--
 Conditionally either load the light or the dark stylesheet. The matching file
 will be downloaded with `highest`, the non-matching file with `lowest`
 priority. If the browser doesn't support `prefers-color-scheme`, the media
 query is unknown and the files are downloaded with `lowest` priority (but
 above I already force `highest` priority for my default light experience).
--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/dark.css"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(prefers-color-scheme: dark)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/light.css"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(prefers-color-scheme: no-preference), (prefers-color-scheme: light)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- The main stylesheet --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/style.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Stylesheet architecture
&lt;/h3&gt;

&lt;p&gt;I make maximum use of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/var"&gt;CSS variables&lt;/a&gt;, this allows my generic style.css to be, well, generic, and all the light or dark mode customization happens in the two other files dark.css and light.css. Below you can see an excerpt of the actual styles, but it should suffice to convey the overall idea. I declare two variables, -⁠-⁠color and -⁠-⁠background-color that essentially create a &lt;em&gt;dark-on-light&lt;/em&gt; and a &lt;em&gt;light-on-dark&lt;/em&gt; baseline theme.&lt;/p&gt;


&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* light.css: 👉 dark-on-light */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="py"&gt;--color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* dark.css: 👉 light-on-dark */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="py"&gt;--color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;In my style.css, I then use these variables in the body { … } rule. As they are defined on the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:root"&gt;:root CSS pseudo-class&lt;/a&gt;—a selector that in HTML represents the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element and is identical to the selector html, except that its specificity is higher—they cascade down, which serves me for declaring global CSS variables.&lt;/p&gt;


&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* style.css */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="py"&gt;color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--background-color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;In the code sample above, you will probably have noticed a property &lt;a href="https://drafts.csswg.org/css-color-adjust-1/#propdef-color-scheme"&gt;color-scheme&lt;/a&gt; with the space-separated value light dark.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; The color-scheme property is still &lt;a href="https://crbug.com/925935"&gt;in development&lt;/a&gt; and it might not work as advertised, full support in Chrome will come later this year.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This tells the browser which color themes my app supports and allows it to activate special variants of the user agent stylesheet, which is useful to, for example, let the browser render form fields with a dark background and light text, adjust the scrollbars, or to enable a theme-aware highlight color. The exact details of color-scheme are specified in &lt;a href="https://drafts.csswg.org/css-color-adjust-1/"&gt;CSS Color Adjustment Module Level 1&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🌒 Read up more on &lt;a href="https://medium.com/dev-channel/what-does-dark-modes-supported-color-schemes-actually-do-69c2eacdfa1d"&gt;what &lt;code&gt;color-scheme&lt;/code&gt; actually does&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything else is then just a matter of defining CSS variables for things that matter on my site. Semantically organizing styles helps a lot when working with dark mode. For example, rather than -⁠-⁠highlight-yellow, consider calling the variable -⁠-⁠accent-color, as "yellow" may actually not be yellow in dark mode or vice versa. Below is an example of some more variables that I use in my example.&lt;/p&gt;


&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* dark.css */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="py"&gt;--color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--link-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;188&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;212&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--main-headline-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;233&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;99&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--accent-background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;188&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;212&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--accent-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* light.css */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="py"&gt;--color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--link-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;238&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--main-headline-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;192&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--accent-background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;238&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="py"&gt;--accent-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Full example
&lt;/h3&gt;

&lt;p&gt;In the following &lt;a href="https://dark-mode-baseline.glitch.me/"&gt;Glitch&lt;/a&gt; embed, you can see the complete example that puts the concepts from above into practice. Try toggling dark mode in your particular &lt;a href="https://web.dev/prefers-color-scheme/#activating-dark-mode-in-the-operating-system"&gt;operating system’s settings&lt;/a&gt; and see how the page reacts.&lt;/p&gt;


&lt;div class="glitch-embed-wrap"&gt;
  &lt;iframe src="https://glitch.com/embed/#!/embed/dark-mode-baseline?previewSize=100&amp;amp;path=index.html" alt="dark-mode-baseline on glitch"&gt;&lt;/iframe&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  Loading impact
&lt;/h3&gt;

&lt;p&gt;When you play with this example, you can see why I load my dark.css and light.css via media queries. Try toggling dark mode and reload the page: the particular currently non-matching stylesheets are still loaded, but with the lowest priority, so that they never compete with resources that are needed by the site right now.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;😲 Read up more on &lt;a href="https://blog.tomayac.com/2018/11/08/why-browsers-download-stylesheets-with-non-matching-media-queries-180513"&gt;why browsers download stylesheets with non-matching media queries&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Axrwmzji--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2Al9ceKHmGLlj7jmxy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Axrwmzji--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2Al9ceKHmGLlj7jmxy.png" alt=""&gt;&lt;/a&gt;Site in light mode loads the dark mode CSS with lowest priority.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_59dZS6S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2A0AmWb_46uTpJ-jk-.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_59dZS6S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2A0AmWb_46uTpJ-jk-.png" alt=""&gt;&lt;/a&gt;Site in dark mode loads the light mode CSS with lowest priority.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jzfaDJY4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2AfJt_2qBrh-Jr2xqF.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jzfaDJY4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2AfJt_2qBrh-Jr2xqF.png" alt=""&gt;&lt;/a&gt;Site in default light mode on a browser that doesn’t support prefers-color-scheme loads the dark mode CSS with lowest priority.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reacting on dark mode changes
&lt;/h3&gt;

&lt;p&gt;Like any other media query change, dark mode changes can be subscribed to via JavaScript. You can use this to, for example, dynamically change the &lt;a href="https://developers.google.com/web/fundamentals/design-and-ux/browser-customization/#provide_great_icons_tiles"&gt;favicon&lt;/a&gt; of a page or change the &lt;a href="https://developers.google.com/web/fundamentals/design-and-ux/browser-customization/#meta_theme_color_for_chrome_and_opera"&gt;&lt;code&gt;&amp;lt;meta name="theme-color"&amp;gt;&lt;/code&gt;&lt;/a&gt; that determines the color of the URL bar in Chrome. The &lt;a href="https://web.dev/prefers-color-scheme/#full-example"&gt;full example&lt;/a&gt; above shows this in action, in order to see the theme color and favicon changes, open the &lt;a href="https://dark-mode-baseline.glitch.me/"&gt;demo in a separate tab&lt;/a&gt;.&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;darkModeMediaQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="nx"&gt;darkModeMediaQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;darkModeOn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Dark mode is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;darkModeOn&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌒 on&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;☀️ off&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Dark mode best practices
&lt;/h3&gt;
&lt;h3&gt;
  
  
  Avoid pure white
&lt;/h3&gt;

&lt;p&gt;A small detail you may have noticed is that I don’t use pure white. Instead, to prevent glowing and bleeding against the surrounding dark content, I choose a slightly darker white. Something like rgb(250, 250, 250) works well.&lt;/p&gt;
&lt;h3&gt;
  
  
  Re-colorize and darken photographic images
&lt;/h3&gt;

&lt;p&gt;If you compare the two screenshots below, you will notice that not only the core theme has changed from &lt;em&gt;dark-on-light&lt;/em&gt; to &lt;em&gt;light-on-dark&lt;/em&gt;, but that also the hero image looks slightly different. My &lt;a href="https://medium.com/dev-channel/re-colorization-for-dark-mode-19e2e17b584b"&gt;user research&lt;/a&gt; has shown that the majority of the surveyed people prefer slightly less vibrant and brilliant images when dark mode is active. I refer to this as &lt;em&gt;re-colorization&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--smP1NKHv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2Awh4lG2vnLdTRcu5U.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--smP1NKHv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2Awh4lG2vnLdTRcu5U.png" alt=""&gt;&lt;/a&gt;Hero image slightly darkened in dark mode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--03529tiO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2AwHfT_my7AgR5o09a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--03529tiO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1024/0%2AwHfT_my7AgR5o09a.png" alt=""&gt;&lt;/a&gt;Regular hero image in light mode.&lt;/p&gt;

&lt;p&gt;Re-colorization can be achieved through a CSS filter on my images. I use a CSS selector that matches all images that don’t have .svg in their URL, the idea being that I can give vector graphics (icons) a different re-colorization treatment than my images (photos), more about this in the &lt;a href="https://web.dev/prefers-color-scheme/#vector-graphics-and-icons"&gt;next paragraph&lt;/a&gt;. Note how I again use a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/var"&gt;CSS variable&lt;/a&gt;, so I can later on flexibly change my filter.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎨 Read up more on &lt;a href="https://medium.com/dev-channel/re-colorization-for-dark-mode-19e2e17b584b"&gt;user research regarding re-colorization preferences with dark mode&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As re-colorization is only needed in dark mode, that is, when dark.css is active, there are no corresponding rules in light.css.&lt;/p&gt;


&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* dark.css */&lt;/span&gt;
&lt;span class="nt"&gt;--image-filter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;grayscale&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;50&lt;/span&gt;&lt;span class="o"&gt;%);&lt;/span&gt;

&lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="nt"&gt;src&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s1"&gt;".svg"&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--image-filter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Customizing dark mode re-colorization intensities with JavaScript
&lt;/h4&gt;

&lt;p&gt;Not everyone is the same and people have different dark mode needs. By sticking to the re-colorization method described above, I can easily make the grayscale intensity a user preference that I can &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties#Values_in_JavaScript"&gt;change via JavaScript&lt;/a&gt;, and by setting a value of 0%, I can also disable re-colorization completely. Note that &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/documentElement"&gt;document.documentElement&lt;/a&gt;provides a reference to the root element of the document, that is, the same element I can reference with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:root"&gt;:rootCSS pseudo-class&lt;/a&gt;.&lt;/p&gt;


&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;const&lt;/span&gt; &lt;span class="nt"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;'grayscale(70%)'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;document&lt;/span&gt;&lt;span class="nc"&gt;.documentElement.style.setProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'--image-filter'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Invert vector graphics and icons
&lt;/h3&gt;

&lt;p&gt;For vector graphics — that in my case are used as icons that I reference via &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements—I use a different re-colorization method. While &lt;a href="https://dl.acm.org/citation.cfm?id=2982168"&gt;research&lt;/a&gt; has shown that people don't like inversion for photos, it does work very well for most icons. Again I use CSS variables to determine the inversion amount in the regular and in the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:hover"&gt;:hover&lt;/a&gt; state.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--epkkiF9---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/744/0%2AtMg3mLZc7MdOoj8c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--epkkiF9---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/744/0%2AtMg3mLZc7MdOoj8c.png" alt=""&gt;&lt;/a&gt;Icons are inverted in dark mode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5dUrXHVx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/744/0%2A9vONLnkqYt3BiDx4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5dUrXHVx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/744/0%2A9vONLnkqYt3BiDx4.png" alt=""&gt;&lt;/a&gt;Regular icons in light mode.&lt;/p&gt;

&lt;p&gt;Note how again I only invert icons in dark.css but not in light.css, and how :hover gets a different inversion intensity in the two cases to make the icon appear slightly darker or slightly brighter, dependent on the mode the user has selected.&lt;/p&gt;


&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* dark.css */&lt;/span&gt;
&lt;span class="nt"&gt;--icon-filter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;invert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%);&lt;/span&gt;
&lt;span class="nt"&gt;--icon-filter_hover&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;invert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;40&lt;/span&gt;&lt;span class="o"&gt;%);&lt;/span&gt;

&lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;src&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s1"&gt;".svg"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--icon-filter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="c"&gt;/* light.css */&lt;/span&gt;
&lt;span class="nt"&gt;--icon-filter_hover&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;invert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;60&lt;/span&gt;&lt;span class="o"&gt;%);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* style.css */&lt;/span&gt;
&lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;src&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s1"&gt;".svg"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--icon-filter_hover&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;currentColor&lt;/code&gt; for inline SVGs
&lt;/h3&gt;

&lt;p&gt;For &lt;em&gt;inline&lt;/em&gt; SVG images, instead of &lt;a href="https://web.dev/prefers-color-scheme/#invert-vector-graphics-and-icons"&gt;using inversion filters&lt;/a&gt;, you can leverage the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#currentColor_keyword"&gt;currentColor&lt;/a&gt; CSS keyword that represents the value of an element's color property. This lets you use the color value on properties that do not receive it by default. Conveniently, if currentColor is used as the value of the SVG &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Fills_and_Strokes#Fill_and_Stroke_Attributes"&gt;fill or&lt;/a&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Fills_and_Strokes#Fill_and_Stroke_Attributes"&gt;strokeattributes&lt;/a&gt;, it instead takes its value from the inherited value of the color property. Even better: this also works for &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use"&gt;&lt;code&gt;&amp;lt;svg&amp;gt;&amp;lt;use href="…"&amp;gt;&amp;lt;/svg&amp;gt;&lt;/code&gt;&lt;/a&gt;, so you can have separate resources and currentColor will still be applied in context. Please note that this only works for &lt;em&gt;inline&lt;/em&gt; or &lt;code&gt;&amp;lt;use href="…"&amp;gt;&lt;/code&gt; SVGs, but not SVGs that are referenced as the src of an image or somehow via CSS. You can see this applied in the demo below.&lt;/p&gt;


&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Some inline SVG --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt;
 &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
 […]
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;div class="glitch-embed-wrap"&gt;
  &lt;iframe src="https://glitch.com/embed/#!/embed/dark-mode-currentcolor?previewSize=100&amp;amp;path=index.html" alt="dark-mode-currentcolor on glitch"&gt;&lt;/iframe&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  Smooth transitions between modes
&lt;/h3&gt;

&lt;p&gt;Switching from dark mode to light mode or vice versa can be smoothed thanks to the fact that both color and background-color are &lt;a href="https://www.quackit.com/css/css3/animations/animatable_properties/"&gt;animatable CSS properties&lt;/a&gt;. Creating the animation is as easy as declaring two transitions for the two properties. The example below illustrates the overall idea, you can experience it live in the&lt;a href="https://dark-mode-baseline.glitch.me/"&gt;demo&lt;/a&gt;.&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="py"&gt;--duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="py"&gt;--timing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--background-color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
 &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--timing&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
 &lt;span class="n"&gt;background-color&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--timing&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Art direction with dark mode
&lt;/h3&gt;

&lt;p&gt;While for loading performance reasons in general I recommend to exclusively work with prefers-color-scheme in the media attribute of &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; elements (rather than inline in stylesheets), there are situations where you actually may want to work with prefers-color-scheme directly inline in your HTML code. Art direction is such a situation. On the web, art direction deals with the overall visual appearance of a page and how it communicates visually, stimulates moods, contrasts features, and psychologically appeals to a target audience.&lt;/p&gt;

&lt;p&gt;With dark mode, it’s up to the judgment of the designer to decide what is the best image at a particular mode and whether &lt;a href="https://web.dev/prefers-color-scheme/#photographic-images"&gt;re-colorization of images&lt;/a&gt; is maybe &lt;em&gt;not&lt;/em&gt; good enough. If used with the &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; element, the &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; of the image to be shown can be made dependent on the media attribute. In the example below, I show the Western hemisphere for dark mode, and the Eastern hemisphere for light mode or when no preference is given, defaulting to the Eastern hemisphere in all other cases. This is of course purely for illustrative purposes. Toggle dark mode on your device to see the difference.&lt;/p&gt;


&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"western.webp"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(prefers-color-scheme: dark)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"eastern.webp"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
 `&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"eastern.webp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;div class="glitch-embed-wrap"&gt;
  &lt;iframe src="https://glitch.com/embed/#!/embed/dark-mode-picture?previewSize=100&amp;amp;path=index.html" alt="dark-mode-picture on glitch"&gt;&lt;/iframe&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  Dark mode, but add an opt-out
&lt;/h3&gt;

&lt;p&gt;As mentioned in the &lt;a href="https://web.dev/prefers-color-scheme/#why-dark-mode"&gt;why dark mode&lt;/a&gt; section above, dark mode is an aesthetic choice for most users. In consequence, some users may actually like to have their operating system UI in dark, but still prefer to see their webpages the way they are used to seeing them. A great pattern is to initially adhere to the signal the browser sends through prefers-color-scheme, but to then optionally allow users to override their system-level setting.&lt;/p&gt;

&lt;h4&gt;
  
  
  The &lt;code&gt;&amp;lt;dark-mode-toggle&amp;gt;&lt;/code&gt; custom element
&lt;/h4&gt;

&lt;p&gt;You can of course create the code for this yourself, but you can also just use a ready-made custom element (web component) that I have created right for this purpose. It’s called &lt;a href="https://github.com/GoogleChromeLabs/dark-mode-toggle"&gt;&lt;code&gt;&amp;lt;dark-mode-toggle&amp;gt;&lt;/code&gt;&lt;/a&gt; and it adds a toggle (dark mode: on/off) or a theme switcher (theme: light/dark) to your page that you can fully customize. The demo below shows the element in action (oh, and I have also 🤫 silently snuck it in all of the &lt;a href="https://dark-mode-baseline.glitch.me/"&gt;other&lt;/a&gt; &lt;a href="https://dark-mode-currentcolor.glitch.me/"&gt;examples&lt;/a&gt; &lt;a href="https://dark-mode-picture.glitch.me/"&gt;above&lt;/a&gt;).&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dark-mode-toggle&lt;/span&gt;
 &lt;span class="na"&gt;legend=&lt;/span&gt;&lt;span class="s"&gt;"Theme Switcher"&lt;/span&gt;
 &lt;span class="na"&gt;appearance=&lt;/span&gt;&lt;span class="s"&gt;"switch"&lt;/span&gt;
 &lt;span class="na"&gt;dark=&lt;/span&gt;&lt;span class="s"&gt;"Dark"&lt;/span&gt;
 &lt;span class="na"&gt;light=&lt;/span&gt;&lt;span class="s"&gt;"Light"&lt;/span&gt;
 &lt;span class="na"&gt;remember=&lt;/span&gt;&lt;span class="s"&gt;"Remember this"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/dark-mode-toggle&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--U0FxTRGJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/140/0%2AuXh_I436B20vKFFc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--U0FxTRGJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/140/0%2AuXh_I436B20vKFFc.png" alt=""&gt;&lt;/a&gt;&amp;lt;dark-mode-toggle&amp;gt; in light mode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--p77Kyrv4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/140/0%2A-YKdMX1aGAEMXKh5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--p77Kyrv4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/140/0%2A-YKdMX1aGAEMXKh5.png" alt=""&gt;&lt;/a&gt;&amp;lt;dark-mode-toggle&amp;gt; in dark mode.&lt;/p&gt;

&lt;p&gt;Try clicking or tapping the dark mode controls in the upper right corner in the demo below. If you check the checkbox in the third and the fourth control, see how your mode selection is remembered even when you reload the page. This allows your visitors to keep their operating system in dark mode, but enjoy your site in light mode or vice versa.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qF2jUiUG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/github-logo-6a5bca60a4ebf959a6df7f08217acd07ac2bc285164fae041eacb8a148b1bab9.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/GoogleChromeLabs"&gt;
        GoogleChromeLabs
      &lt;/a&gt; / &lt;a href="https://github.com/GoogleChromeLabs/dark-mode-toggle"&gt;
        dark-mode-toggle
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A custom element that allows you to easily put a Dark Mode 🌒 toggle or switch on your site:
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="instapaper_body md"&gt;
&lt;p&gt;&lt;a href="https://www.webcomponents.org/element/dark-mode-toggle" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/e92dc407af2fd15cebe3fb7449c112c0bdc05e63/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f776562636f6d706f6e656e74732e6f72672d7075626c69736865642d626c75652e737667" alt="Published on webcomponents.org"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
&lt;code&gt;&amp;lt;dark-mode-toggle&amp;gt;&lt;/code&gt; Element&lt;/h1&gt;
&lt;p&gt;A custom element that allows you to easily put a &lt;em&gt;Dark Mode 🌒&lt;/em&gt; toggle
or switch on your site, so you can initially adhere to your users' preferences according to
&lt;a href="https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme" rel="nofollow"&gt;&lt;code&gt;prefers-color-scheme&lt;/code&gt;&lt;/a&gt;
but also allow them to (optionally permanently) override their system setting for just your site.&lt;/p&gt;
&lt;p&gt;📚 Read all(!) about dark mode in the related article
&lt;a href="https://web.dev/prefers-color-scheme/" rel="nofollow"&gt;Hello Darkness, My Old Friend&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
Installation&lt;/h2&gt;
&lt;p&gt;From npm:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;npm install --save dark-mode-toggle&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In the browser:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-c1"&gt;*&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-smi"&gt;DarkModeToggle&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;https://cdn.pika.dev/dark-mode-toggle&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-c1"&gt;*&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-smi"&gt;DarkModeToggle&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;https://unpkg.com/dark-mode-toggle&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;
Usage&lt;/h2&gt;
&lt;p&gt;⚠️ The custom element assumes that you have organized your CSS in different files
that you load conditionally based on the &lt;strong&gt;&lt;code&gt;media&lt;/code&gt;&lt;/strong&gt; attribute in the stylesheet's
corresponding &lt;code&gt;link&lt;/code&gt; element. This is a great performance pattern
as you don't force people to download CSS that they don't need
based on their current theme preference, yet non-matching…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/GoogleChromeLabs/dark-mode-toggle"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusions
&lt;/h3&gt;

&lt;p&gt;Working with and supporting dark mode is fun and opens up new design avenues. For some of your visitors it can be the difference between not being able to handle your site and being a happy user. There are some pitfalls and careful testing is definitely required, but dark mode is definitely a great opportunity for you to show that you care about all of your users. The best practices mentioned in this post and helpers like the &lt;a href="https://github.com/GoogleChromeLabs/dark-mode-toggle"&gt;&lt;code&gt;&amp;lt;dark-mode-toggle&amp;gt;&lt;/code&gt;&lt;/a&gt; custom element should make you confident in your ability to create an amazing dark mode experience. &lt;a href="https://twitter.com/tomayac"&gt;Let me know on Twitter&lt;/a&gt;what you create and if this post was useful or also suggestions for improving it. Thanks for reading! 🌒&lt;/p&gt;

&lt;h3&gt;
  
  
  Related links
&lt;/h3&gt;

&lt;p&gt;Resources for the prefers-color-scheme media query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://chromestatus.com/feature/5109758977638400"&gt;Chrome Platform Status page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://crbug.com/889087"&gt;Chromium bug&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme"&gt;Media Queries Level 5 spec&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Resources for the color-scheme meta tag and CSS property:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://chromestatus.com/feature/5330651267989504"&gt;Chrome Platform Status page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://crbug.com/925935"&gt;Chromium bug&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drafts.csswg.org/css-color-adjust-1/"&gt;CSS Color Adjustment Module Level 1 spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/w3c/csswg-drafts/issues/3299"&gt;CSS WG GitHub Issue for the meta tag and the CSS property&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/whatwg/html/issues/4504"&gt;HTML WHATWG GitHub Issue for the meta tag&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;General dark mode links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://material.io/design/color/dark-theme.html"&gt;Material Design — Dark Theme&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webkit.org/blog/8892/dark-mode-in-web-inspector/"&gt;Dark Mode in Web Inspector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webkit.org/blog/8840/dark-mode-support-in-webkit/"&gt;Dark Mode Support in WebKit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/dark-mode/"&gt;Apple Human Interface Guidelines — Dark Mode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Background research articles for this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://medium.com/dev-channel/what-does-dark-modes-supported-color-schemes-actually-do-69c2eacdfa1d"&gt;What Does Dark Mode’s “supported-color-schemes” Actually Do? 🤔&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/dev-channel/let-there-be-darkness-maybe-9facd9c3023d"&gt;Let there be darkness! 🌚 Maybe…&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/dev-channel/re-colorization-for-dark-mode-19e2e17b584b"&gt;Re-Colorization for Dark Mode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Acknowledgements
&lt;/h3&gt;

&lt;p&gt;The prefers-color-scheme media feature, the color-scheme CSS property, and the related meta tag are the implementation work of 👏 &lt;a href="https://twitter.com/runeli"&gt;Rune Lillesveen&lt;/a&gt;. Rune is also a co-editor of the &lt;a href="https://drafts.csswg.org/css-color-adjust-1/"&gt;CSS Color Adjustment Module Level 1&lt;/a&gt;spec. I would like to 🙏 thank &lt;a href="https://www.linkedin.com/in/lukasz-zbylut/"&gt;Lukasz Zbylut&lt;/a&gt;, &lt;a href="https://twitter.com/rowan_m"&gt;Rowan Merewood&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/chiragd/"&gt;Chirag Desai&lt;/a&gt;, and &lt;a href="https://twitter.com/rob_dodson"&gt;Rob Dodson&lt;/a&gt; for their thorough reviews of this article. The &lt;a href="https://web.dev/prefers-color-scheme/#loading-strategy"&gt;loading strategy&lt;/a&gt; is the brainchild of &lt;a href="https://twitter.com/jaffathecake"&gt;Jake Archibald&lt;/a&gt;. &lt;a href="https://twitter.com/ecbos_"&gt;Emilio Cobos Álvarez&lt;/a&gt; has pointed me to the correct prefers-color-schemedetection method. The tip with referenced SVGs and currentColor came from &lt;a href="https://twitter.com/xeenon"&gt;Timothy Hatcher&lt;/a&gt;. Finally, I am thankful to the many anonymous participants of the various user studies that have helped shape the recommendations in this article. Hero image by &lt;a href="https://unsplash.com/photos/kujXUuh1X0o"&gt;Nathan Anderson&lt;/a&gt;.&lt;/p&gt;




</description>
      <category>preferscolorscheme</category>
      <category>darkmode</category>
      <category>darktheme</category>
      <category>css</category>
    </item>
    <item>
      <title>Exposing headers on CORS responses</title>
      <dc:creator>Jeff Posnick</dc:creator>
      <pubDate>Mon, 22 Jul 2019 12:00:00 +0000</pubDate>
      <link>https://dev.to/chromiumdev/exposing-headers-on-cors-responses-1k8j</link>
      <guid>https://dev.to/chromiumdev/exposing-headers-on-cors-responses-1k8j</guid>
      <description>&lt;h2&gt;
  
  
  CORS and its discontents
&lt;/h2&gt;

&lt;p&gt;The concept of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"&gt;CORS&lt;/a&gt; requests comes up a lot in my professional life. Much of the time, it's in the context of why a given response is &lt;a href="https://stackoverflow.com/questions/39109789/what-limitations-apply-to-opaque-responses"&gt;opaque&lt;/a&gt;, and how to make that response non-opaque so that it plays nicely with service workers and the Cache Storage API.&lt;/p&gt;

&lt;p&gt;Fortunately, many popular third-party APIs and hosts support CORS nowadays, and solving your basic CORS-related mystery normally boils down to, say, adding in the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes"&gt;&lt;code&gt;crossorigin&lt;/code&gt; attribute&lt;/a&gt; to your &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;

&lt;h2&gt;
  
  
  When CORS is not enough
&lt;/h2&gt;

&lt;p&gt;But while enabling CORS is enough to get back basic information about an HTTP response—like its status code, or access to its body—there's still some information that's locked down by default. The headers exposed on a CORS response, for instance, are limited to the following six &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Simple_response_header"&gt;"simple" response headers&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cache-Control&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Language&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Expires&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Last-Modified&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Pragma&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of those headers can come in handy when accessed inside of a service worker, but there's one in particular that can be useful, but isn't exposed by default: &lt;code&gt;Date&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In particular, if you're using &lt;a href="https://developers.google.com/web/tools/workbox/modules/workbox-cache-expiration#restrict_the_age_of_cached_entries"&gt;Workbox's cache expiration&lt;/a&gt; logic and you provide a &lt;code&gt;maxAgeSeconds&lt;/code&gt; parameter, the &lt;code&gt;Date&lt;/code&gt; of the cached response &lt;a href="https://github.com/GoogleChrome/workbox/blob/b0825d74d81264e7b4537ed170dd60de638561ba/packages/workbox-expiration/src/Plugin.ts#L176-L195"&gt;is checked&lt;/a&gt; against the difference between the current time and &lt;code&gt;maxAgeSeconds&lt;/code&gt;. If the &lt;code&gt;Date&lt;/code&gt; is too old, then the cached response will end up being ignored.&lt;/p&gt;

&lt;p&gt;But... this logic only works if there's a &lt;code&gt;Date&lt;/code&gt; header exposed on the response. By default, that won't be the case for a CORS response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exposition
&lt;/h2&gt;

&lt;p&gt;The workaround, as with so many things related to CORS, involves fiddling with HTTP response headers. You'll either need access to the underlying HTTP server yourself, or you'll need to reach out to your CDN/API provider asking them to make the change.&lt;/p&gt;

&lt;p&gt;Setting &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers"&gt;&lt;code&gt;Access-Control-Expose-Headers: Date&lt;/code&gt;&lt;/a&gt; will permit the &lt;code&gt;Date&lt;/code&gt; response header to be visible to your web app's code, and you could include any additional headers there in a comma-separated list.&lt;/p&gt;

&lt;p&gt;If you're using your own Express-based web server, the &lt;a href="https://www.npmjs.com/package/corser"&gt;&lt;code&gt;corser&lt;/code&gt; middleware&lt;/a&gt; looks pretty reasonable for setting up a working configu ration. Their docs include a &lt;a href="https://www.npmjs.com/package/corser#getting-a-response-header-returns-refused-to-get-unsafe-header-x"&gt;recipe&lt;/a&gt; for configuring the exposed response headers.&lt;/p&gt;

</description>
      <category>cors</category>
      <category>javascript</category>
      <category>post</category>
      <category>workbox</category>
    </item>
    <item>
      <title>Measuring Cumulative Layout Shift (CLS) in WebPageTest</title>
      <dc:creator>Rick Viscomi</dc:creator>
      <pubDate>Thu, 18 Jul 2019 21:36:45 +0000</pubDate>
      <link>https://dev.to/chromiumdev/measuring-cumulative-layout-shift-cls-in-webpagetest-5cle</link>
      <guid>https://dev.to/chromiumdev/measuring-cumulative-layout-shift-cls-in-webpagetest-5cle</guid>
      <description>&lt;h1&gt;
  
  
  What is CLS?
&lt;/h1&gt;

&lt;p&gt;Cumulative Layout Shift (CLS) is a relatively new metric to help you understand the &lt;a href="https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics#user-centric_performance_metrics" rel="noopener noreferrer"&gt;delightfulness&lt;/a&gt; of the user experience. You might be reading a website when suddenly the text shifts down 100 pixels and you lose your place. That's not a delightful experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fweb.dev%2Flayout-instability-api%2Flayout-shift-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fweb.dev%2Flayout-instability-api%2Flayout-shift-1.png" alt="A graphic showing text shifting down"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The value of a layout shift is the percent of the viewport that moved. So in this example perhaps 10% of the viewport shifted. If another shift of 2% happens later, the page's CLS would be 12%.&lt;/p&gt;

&lt;h1&gt;
  
  
  How can it be measured?
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://web.dev/layout-instability-api" rel="noopener noreferrer"&gt;web.dev/layout-instability/api&lt;/a&gt; is a great resource to learn more about the API and how to use it to measure CLS. Here's their example code to calculate CLS:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// Stores the current layout shift score for the page.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cumulativeLayoutShiftScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Detects new layout shift occurrences and updates the&lt;/span&gt;
&lt;span class="c1"&gt;// `cumulativeLayoutShiftScore` variable.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cumulativeLayoutShiftScore&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;entryTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;layoutShift&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]});&lt;/span&gt;

&lt;span class="c1"&gt;// Sends the final score to your analytics back end once&lt;/span&gt;
&lt;span class="c1"&gt;// the page's lifecycle state becomes hidden.&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibilityState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Force any pending records to be dispatched.&lt;/span&gt;
    &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;takeRecords&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Send the final score to your analytics back end&lt;/span&gt;
    &lt;span class="c1"&gt;// (assumes `sendToAnalytics` is defined elsewhere).&lt;/span&gt;
    &lt;span class="nf"&gt;sendToAnalytics&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;cumulativeLayoutShiftScore&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;At the moment, the &lt;code&gt;layoutShift&lt;/code&gt; type is only available when the API is explicitly enabled. If you're running Chrome 76 or newer, you can start it up via the command line with this flag set:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

--enable-blink-features=LayoutInstabilityAPI


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  Measuring CLS in WebPageTest
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://webpagetest.org" rel="noopener noreferrer"&gt;WebPageTest&lt;/a&gt; (WPT) is a popular tool for controlled measurements of web performance stats. Let's take a look at how we can configure it to record CLS.&lt;/p&gt;
&lt;h2&gt;
  
  
  Choosing the right browser
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fefdsz9sly0ep4dpik3kw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fefdsz9sly0ep4dpik3kw.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;WPT supports many browsers from many locations around the world. For this test to work, we need a newer version of Chrome that supports the &lt;code&gt;LayoutInstabilityAPI&lt;/code&gt; Blink feature. Since WPT's default version of Chrome is on version 75 at this time of writing, we'll select Chrome Canary (version 77), which is available from the Dulles, VA test location.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting the command line flag
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdsw5dpja7ghywoha474o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdsw5dpja7ghywoha474o.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the advanced settings "Chromium" tab there's a field to provide command line options.&lt;/p&gt;
&lt;h2&gt;
  
  
  Measuring CLS as a custom metric
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupdyyf89imz3uly3pnkv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupdyyf89imz3uly3pnkv.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Custom metrics are snippets of JavaScript that execute as the web page test is wrapping up. The syntax is to give it a name in &lt;code&gt;[name]&lt;/code&gt; format on the first line, followed by a script with a single return value. Here's a custom metric to measure CLS:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CumulativeLayoutShift&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;CLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hadRecentInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;CLS&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CLS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;layout-shift&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Side note: Astute WPT mega-pro power users reading this post are objecting to this script because it returns a promise, and custom metrics only support synchronous code. Not anymore! As of literally &lt;a href="https://github.com/WPO-Foundation/wptagent/commit/19f88b560a55e2e31b718c89efdd9e72b432fb89" rel="noopener noreferrer"&gt;a couple hours ago&lt;/a&gt;, WPT now supports async custom metrics!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This script will resolve to the CLS value for the test page, whose layout shifts have been saved to a buffer.&lt;/p&gt;




&lt;p&gt;Ok! So with those options configured, you're ready to enter the URL of the page you want to test and run it. You can find the results on the &lt;a href="https://www.webpagetest.org/custom_metrics.php?test=190718_R6_69b9deac766918ca221a7c1b66326ed2&amp;amp;run=1&amp;amp;cached=0" rel="noopener noreferrer"&gt;Custom Metrics&lt;/a&gt; page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fekaovlb56l8jbwzynon3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fekaovlb56l8jbwzynon3.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this page, the CLS is &lt;code&gt;0.3&lt;/code&gt;. It's a page I built, with a large part of its UI dynamically rendered from a script, so ~30% checks out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffrjn9udghozm57i42gfu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffrjn9udghozm57i42gfu.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To visualize this, we can use the &lt;a href="https://www.webpagetest.org/video/compare.php?tests=190718_R6_69b9deac766918ca221a7c1b66326ed2-r:1-c:0" rel="noopener noreferrer"&gt;filmstrip view&lt;/a&gt;, which shows us what the page looked like while it loaded. In this case it was blank for half a second, then the entire thing rendered. It's not clear where the layout shift is, but it could have happened in a matter of milliseconds. It isn't necessarily a poor user experience if the layout shift happens so close to the start of rendering, so that might be a case for tweaking the CLS algorithm.&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;CLS is still a very new metric, so if you do start using it, please leave any feedback on the &lt;a href="https://github.com/WICG/layout-instability" rel="noopener noreferrer"&gt;Layout Instability API GitHub repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webperf</category>
      <category>webpagetest</category>
    </item>
    <item>
      <title>New "State of the Web" video: BigQuery!</title>
      <dc:creator>Rick Viscomi</dc:creator>
      <pubDate>Thu, 04 Jul 2019 23:08:15 +0000</pubDate>
      <link>https://dev.to/chromiumdev/video-bigquery-the-state-of-the-web-3ejl</link>
      <guid>https://dev.to/chromiumdev/video-bigquery-the-state-of-the-web-3ejl</guid>
      <description>&lt;p&gt;When we pull stats about the state of the web, like the percent of websites adopting HTTPS or the median amount of image bytes per page, these figures come from web transparency datasets on BigQuery like the HTTP Archive and Chrome UX Report. In this video, I chat with Felipe Hoffa (BigQuery Developer Advocate) about the capabilities of BigQuery and how to get started with it.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/dmD9_mN94eM"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>bigquery</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Disable a HTML form while in-flight using fieldset</title>
      <dc:creator>Sam Thorogood</dc:creator>
      <pubDate>Tue, 11 Jun 2019 03:21:49 +0000</pubDate>
      <link>https://dev.to/chromiumdev/disable-a-html-form-while-in-flight-using-fieldset-61b</link>
      <guid>https://dev.to/chromiumdev/disable-a-html-form-while-in-flight-using-fieldset-61b</guid>
      <description>&lt;p&gt;I'm a big fan of great UIs that properly control your user's ability to interact with your page: such as changing some value during a form submit. We've all seen the &lt;em&gt;"do not change this form until it's done!"&lt;/em&gt; messages. 🙄&lt;/p&gt;

&lt;p&gt;There's a standard HTML feature which can help you with this, in &lt;code&gt;&amp;lt;fieldset disabled&amp;gt;&lt;/code&gt;. First, here's a demo:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/samthor/embed/LKYRGy?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Try submitting the form, clicking on one of the blue links (they don't go anywhere, it's just for the demo) and pressing the Tab key. The inputs cannot be focused! 🤯&lt;/p&gt;

&lt;h2&gt;
  
  
  The Feature
&lt;/h2&gt;

&lt;p&gt;When your browser sees a &lt;code&gt;&amp;lt;fieldset&amp;gt;&lt;/code&gt; with its &lt;code&gt;disabled&lt;/code&gt; attribute set: e.g., &lt;code&gt;&amp;lt;fieldset disabled&amp;gt;&lt;/code&gt;, it will completely disable every &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; element found within. 🚫📝&lt;/p&gt;

&lt;p&gt;This is pretty powerful. Your code might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;yourForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// disable normal submit&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yourForm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/foo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// important bit here&lt;/span&gt;
  &lt;span class="nx"&gt;yourFieldset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// .finally runs even if the fetch fails&lt;/span&gt;
    &lt;span class="nx"&gt;yourFieldset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other uses for &lt;code&gt;fieldset&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;fieldset&lt;/code&gt; element also lets you group HTML form elements, including adding a &lt;code&gt;legend&lt;/code&gt;(-ary). It's useful for accessibility and as a way to visually style groups. For a simple demo, check out &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset"&gt;this page on MDN&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives
&lt;/h2&gt;

&lt;p&gt;Classic alternatives to really simple, &lt;em&gt;powerful&lt;/em&gt; features like &lt;code&gt;&amp;lt;fieldset disabled&amp;gt;&lt;/code&gt; tend to include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adding a giant element that covers the form so users can't click on it&lt;/li&gt;
&lt;li&gt;iterating through every &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; and marking it &lt;code&gt;disabled&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;just hiding the form.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And all of these are pretty painful versus &lt;code&gt;&amp;lt;fieldset disabled&amp;gt;&lt;/code&gt;. 🤮&lt;/p&gt;

&lt;h2&gt;
  
  
  Thanks!
&lt;/h2&gt;

&lt;p&gt;If you're curious about controlling user interaction, be sure to read up on the "inert" attribute, which takes &lt;code&gt;&amp;lt;fieldset disabled&amp;gt;&lt;/code&gt; one step further, but doesn't have full browser support yet.&lt;/p&gt;

&lt;p&gt;11 👋&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>fieldset</category>
    </item>
    <item>
      <title>Beyond appendChild: Better convenience methods for HTML</title>
      <dc:creator>Sam Thorogood</dc:creator>
      <pubDate>Sun, 09 Jun 2019 00:09:35 +0000</pubDate>
      <link>https://dev.to/chromiumdev/beyond-appendchild-better-convenience-methods-for-html-55n4</link>
      <guid>https://dev.to/chromiumdev/beyond-appendchild-better-convenience-methods-for-html-55n4</guid>
      <description>&lt;p&gt;I've built on the web for a while now. So long that when I'm writing vanilla HTML/JS, my go-to has always been &lt;code&gt;.appendChild()&lt;/code&gt; to add new HTML elements, and a huge series of &lt;code&gt;createElement&lt;/code&gt; calls along with it.&lt;/p&gt;

&lt;p&gt;But there's actually some lesser-known convenience methods that we can use now (well, in a post-IE11 world, where all devs should be). 🌎👍 I'm not arguing against your framework or components, but sometimes, you &lt;em&gt;just have to write vanilla JS&lt;/em&gt;. 🍦&lt;/p&gt;

&lt;h2&gt;
  
  
  One-Line Element Creation
&lt;/h2&gt;

&lt;p&gt;I confess 😅 that this isn't really one line, but it's one &lt;em&gt;statement&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Your div now has text`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;and-is-classy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The helper &lt;code&gt;Object.assign&lt;/code&gt; wasn't available in IE11.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remove Self
&lt;/h2&gt;

&lt;p&gt;This one is pretty well-known.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;something&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// instead of el.parentNode.removeChild(el)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Insert Element &lt;em&gt;or&lt;/em&gt; Text
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.append()&lt;/code&gt; method can append any real elements, or it will automatically create a text node if you pass it a string. It takes any number of arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;I get upgraded to a text node!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;.prepend()&lt;/code&gt; which is the opposite of &lt;code&gt;.append()&lt;/code&gt;. It inserts all the elements, in-order, at the start of the element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;List Of Awesome HTML Methods&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`You Won't Believe How Many We Found!`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Insert Relative To Element
&lt;/h2&gt;

&lt;p&gt;Every element has methods &lt;code&gt;.before()&lt;/code&gt; and &lt;code&gt;.after()&lt;/code&gt;. These insert new HTML nodes directly adjacent to the current node. Like the methods above, they accept any number of other elements or strings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;myHeading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;superHeading&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;myHeading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Here's a list of awesome stuff`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theList&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ There's one caveat: in our example, if &lt;code&gt;myHeading&lt;/code&gt; isn't actually on the page—it's a temporary element—these methods will just fail silently without throwing an &lt;code&gt;Error&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replace Self
&lt;/h2&gt;

&lt;p&gt;Rather than doing a &lt;code&gt;parentNode.replaceChild&lt;/code&gt; dance, we can now self-destruct an element and replace it with something new. Again, we can replace ourselves with any number of other elements or strings (even none!).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fancyItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;strong&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fancy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;someFancyHeading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Less&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fancyItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;someFancyHeading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceWith&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// although you could just use .remove 🤷&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Class Force Set
&lt;/h2&gt;

&lt;p&gt;If you want to set the state of a class to a variable true or false, you can pass a second param to &lt;code&gt;.classList.toggle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;someState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;theDiv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;someState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// forces foo on&lt;/span&gt;
&lt;span class="nx"&gt;theDev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;someState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// forces bar off&lt;/span&gt;
&lt;span class="c1"&gt;// result e.g. &amp;lt;div id="theDiv" class="foo"&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;em&gt;probably&lt;/em&gt; well-known. But if you're explicitly not supporting IE11, it's nice to be confident that this now works. 🎉&lt;/p&gt;

&lt;h1&gt;
  
  
  Done!
&lt;/h1&gt;

&lt;p&gt;What have I missed? Let me know if there any other old habits you've recently discovered you can let go of.&lt;/p&gt;

&lt;p&gt;9 👋&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>html</category>
    </item>
  </channel>
</rss>
