<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>JavaScript on Smashing Magazine — For Web Designers And Developers</title><link>https://www.smashingmagazine.com/category/javascript/index.xml</link><description>Recent content in JavaScript on Smashing Magazine — For Web Designers And Developers</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 09 Feb 2026 03:03:08 +0000</lastBuildDate><item><author>Mat Marquis</author><title>JavaScript For Everyone: Iterators</title><link>https://www.smashingmagazine.com/2025/10/javascript-for-everyone-iterators/</link><pubDate>Mon, 27 Oct 2025 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/10/javascript-for-everyone-iterators/</guid><description>Here is a lesson on Iterators: iterables implement the iterable iteration interface, and iterators implement the iterator iteration interface. Sounds confusing? Mat breaks it all down in the article.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/10/javascript-for-everyone-iterators/" />
              <title>JavaScript For Everyone: Iterators</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>JavaScript For Everyone: Iterators</h1>
                  
                    
                    <address>Mat Marquis</address>
                  
                  <time datetime="2025-10-27T13:00:00&#43;00:00" class="op-published">2025-10-27T13:00:00+00:00</time>
                  <time datetime="2025-10-27T13:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Hey, I’m Mat, but “Wilto” works too &mdash; I’m here to teach you JavaScript. Well, not <em>here</em>-here; technically, I’m over at <a href="https://piccalil.li/javascript-for-everyone">Piccalil.li’s <em>JavaScript for Everyone</em></a> course to teach you JavaScript. The following is an excerpt from the <strong>Iterables and Iterators</strong> module: the lesson on Iterators.</p>

<p>Iterators are one of JavaScript’s more linguistically confusing topics, sailing <em>easily</em> over what is already a pretty high bar. There are <em>iterables</em> &mdash; array, Set, Map, and string &mdash; all of which follow the <strong>iterable protocol</strong>. To follow said protocol, an object must implement the <strong>iterable interface</strong>. In practice, that means that the object needs to include a <code>[Symbol.iterator]()</code> method somewhere in its prototype chain. Iterable protocol is one of two <strong>iteration protocols</strong>. The other iteration protocol is the <strong>iterator protocol</strong>.</p>

<p>See what I mean about this being linguistically fraught? Iterables implement the iterable iteration interface, and iterators implement the iterator iteration interface! If you can say that five times fast, then you’ve pretty much got the gist of it; easy-peasy, right?</p>

<p>No, listen, by the time you reach the end of this lesson, I promise it won’t be half as confusing as it might sound, especially with the context you’ll have from the lessons that precede it.</p>

<p>An <strong>iterable</strong> object follows the iterable protocol, which just means that the object has a conventional method for making iterators. The elements that it contains can be looped over with <code>for</code>…<code>of</code>.</p>

<p>An <strong>iterator</strong> object follows the iterator protocol, and the elements it contains can be accessed <em>sequentially</em>, one at a time.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Roll up your sleeves and <strong>boost your UX skills</strong>! Meet <strong><a data-instant href="https://smart-interface-design-patterns.com/">Smart Interface Design Patterns</a></strong>&nbsp;🍣, a 10h video library by Vitaly Friedman. <strong>100s of real-life examples</strong> and live UX training. <a href="https://www.youtube.com/watch?v=3mwZztmGgbE">Free preview</a>.</p>
<a data-instant href="https://smart-interface-design-patterns.com/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://smart-interface-design-patterns.com/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3155f571-450d-42f9-81b4-494aa9b52841/video-course-smart-interface-design-patterns-vitaly-friedman.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8c98e7f9-8e62-4c43-b833-fc6bf9fea0a9/video-course-smart-interface-design-patterns-vitaly-friedman.jpg"
    alt="Feature Panel"
    width="690"
    height="790"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<p>To <em>reiterate</em> &mdash; a play on words for which I do not forgive myself, nor expect you to forgive me &mdash; an <strong>iterator</strong> object follows iterator protocol, and the elements it contains can be accessed <em>sequentially</em>, one at a time. Iterator protocol defines a standard way to produce a sequence of values, and optionally <code>return</code> a value once all possible values have been generated.</p>

<p>In order to follow the iterator protocol, an object has to &mdash; you guessed it &mdash; implement the <strong>iterator interface</strong>. In practice, that once again means that a certain method has to be available somewhere on the object&rsquo;s prototype chain. In this case, it’s the <code>next()</code> method that advances through the elements it contains, one at a time, and returns an object each time that method is called.</p>

<p>In order to meet the iterator interface criteria, the returned object must contain two properties with specific keys: one with the key <code>value</code>, representing the value of the current element, and one with the key <code>done</code>, a Boolean value that tells us if the iterator has advanced beyond the final element in the data structure. That’s not an awkward phrasing the editorial team let slip through: the value of that <code>done</code> property is <code>true</code> only when a call to <code>next()</code> results in an attempt to access an element <em>beyond</em> the final element in the iterator, not upon accessing the final element in the iterator. Again, a lot in print, but it’ll make more sense when you see it in action.</p>

<p>You’ve seen an example of a built-in iterator before, albeit briefly:</p>

<pre><code class="language-jsx">const theMap = new Map([ [ "aKey", "A value." ] ]);

console.log( theMap.keys() );
// Result: Map Iterator { constructor: Iterator() }
</code></pre>

<p>That’s right: while a Map object itself is an iterable, Map’s built-in methods <code>keys()</code>, <code>values()</code>, and <code>entries()</code> all return Iterator objects. You’ll also remember that I looped through those using <code>forEach</code> (a relatively recent addition to the language). Used that way, an iterator is indistinguishable from an iterable:</p>

<pre><code class="language-jsx">const theMap = new Map([ [ "key", "value " ] ]);

theMap.keys().forEach( thing =&gt; {
  console.log( thing );
});
// Result: key
</code></pre>

<p>All iterators are iterable; they all implement the iterable interface:</p>

<pre><code class="language-jsx">const theMap = new Map([ [ "key", "value " ] ]);

theMap.keys()[ Symbol.iterator ];
// Result: function Symbol.iterator()
</code></pre>

<p>And if you’re angry about the increasing blurriness of the line between iterators and iterables, wait until you get a load of this “top ten anime betrayals” video candidate: I’m going to demonstrate how to interact with an iterator by using an array.</p>

<p>“BOO,” you surely cry, having been so betrayed by one of your oldest and most indexed friends. “Array is an itera<em>ble</em>, not an itera<em>tor</em>!” You are both right to yell at me in general, and right about array in specific &mdash; an array <em>is</em> an iterable, not an iterator. In fact, while all iterators are iterable, none of the built-in iterables are iterators.</p>

<p>However, when you call that <code>[ Symbol.iterator ]()</code> method &mdash; the one that defines an object as an iterable &mdash; it returns an iterator object created from an iterable data structure:</p>

<pre><code class="language-jsx">const theIterable = [ true, false ];
const theIterator = theIterable[ Symbol.iterator ]();

theIterable;
// Result: Array [ true, false ]

theIterator;
// Result: Array Iterator { constructor: Iterator() }
</code></pre>

<p>The same goes for Set, Map, and &mdash; yes &mdash; even strings:</p>

<pre><code class="language-jsx">const theIterable = "A string."
const theIterator = theIterable[ Symbol.iterator ]();

theIterator;
// Result: String Iterator { constructor: Iterator() }
</code></pre>

<p>What we’re doing here manually &mdash; creating an iterator from an iterable using <code>%Symbol.iterator%</code> &mdash; is precisely how iterable objects work internally, and why they have to implement <code>%Symbol.iterator%</code> in order to <em>be</em> iterables. Any time you loop through an array, you’re actually looping through an iterator created from that Array. All built-in iterators <em>are</em> iterable. All built-in iterables can be used to <em>create</em> iterators.</p>

<p>Alternately &mdash; <em>preferably</em>, even, since it doesn’t require you to graze up against <code>%Symbol.iterator%</code> directly &mdash; you can use the built-in <code>Iterator.from()</code> method to create an iterator object from any iterable:</p>

<pre><code class="language-jsx">const theIterator = Iterator.from([ true, false ]);

theIterator;
// Result: Array Iterator { constructor: Iterator() }
</code></pre>

<p>You remember how I mentioned that an iterator has to provide a <code>next()</code> method (that returns a very specific Object)? Calling that <code>next()</code> method steps through the elements that the iterator contains one at a time, with each call returning an instance of that Object:</p>

<pre><code class="language-jsx">const theIterator = Iterator.from([ 1, 2, 3 ]);

theIterator.next();
// Result: Object { value: 1, done: false }

theIterator.next();
// Result: Object { value: 2, done: false }

theIterator.next();
// Result: Object { value: 3, done: false }

theIterator.next();
// Result: Object { value: undefined, done: true }
</code></pre>

<p>You can think of this as a more controlled form of traversal than the traditional “wind it up and watch it go” <code>for</code> loops you’re probably used to &mdash; a method of accessing elements one step at a time, as-needed. Granted, you don’t <em>have</em> to step through an iterator in this way, since they have their very own <code>Iterator.forEach</code> method, which works exactly like you would expect &mdash; to a point:</p>

<pre><code class="language-jsx">const theIterator = Iterator.from([ true, false ]);

theIterator.forEach( element =&gt; console.log( element ) );
/&#42; Result:
true
false
&#42;/
</code></pre>

<p>But there’s another big difference between iterables and iterators that we haven’t touched on yet, and for my money, it actually goes a long way toward making <em>linguistic</em> sense of the two. You might need to humor me for a little bit here, though.</p>

<p>See, an iterable object is an object that is iterable. No, listen, stay with me: you can iterate over an Array, and when you’re done doing so, you can still iterate over that Array. It is, by definition, an object that can be iterated over; it is the essential nature of an iterable to be iterable:</p>

<pre><code class="language-jsx">const theIterable = [ 1, 2 ];

theIterable.forEach( el =&gt; {
  console.log( el );
});
/&#42; Result:
1
2
&#42;/

theIterable.forEach( el =&gt; {
  console.log( el );
});
/&#42; Result:
1
2
&#42;/
</code></pre>

<p>In a way, an iterator object represents the singular <em>act</em> of iteration. Internal to an iterable, it is the mechanism by which the iterable is iterated over, each time that iteration is performed. As a stand-alone iterator object &mdash; whether you step through it using the <code>next</code> method or loop over its elements using <code>forEach</code> &mdash; once iterated over, that iterator is <em>past tense</em>; it is <em>iterated</em>. Because they maintain an internal state, the essential nature of an iterator is to be iterated over, singular:</p>

<pre><code class="language-jsx">const theIterator = Iterator.from([ 1, 2 ]);

theIterator.next();
// Result: Object { value: 1, done: false }

theIterator.next();
// Result: Object { value: 2, done: false }

theIterator.next();
// Result: Object { value: undefined, done: true }

theIterator.forEach( el =&gt; console.log( el ) );
// Result: undefined
</code></pre>

<p>That makes for neat work when you&rsquo;re using the Iterator constructor’s built-in methods to, say, filter or extract part of an Iterator object:</p>

<div class="break-out">
<pre><code class="language-jsx">const theIterator = Iterator.from([ "First", "Second", "Third" ]);

// Take the first two values from `theIterator`:
theIterator.take( 2 ).forEach( el =&gt; {
  console.log( el );
});
/&#42; Result:
"First"
"Second"
&#42;/

// theIterator now only contains anything left over after the above operation is complete:
theIterator.next();
// Result: Object { value: "Third", done: false }
</code></pre>
</div>

<p>Once you reach the end of an iterator, the act of iterating over it is complete. Iterated. Past-tense.</p>

<p>And so too is your time in this lesson, you might be relieved to hear. I know this was kind of a rough one, but the good news is: this course is iterable, not an iterator. This step in your iteration through it &mdash; this lesson &mdash; may be over, but the essential nature of this course is that you can iterate through it again. Don’t worry about committing all of this to memory right now &mdash; you can come back and revisit this lesson anytime.</p>

<div class="partners__lead-place"></div>

<h2 id="conclusion">Conclusion</h2>

<p>I stand by what I wrote there, unsurprising as that probably is: this lesson is a tricky one, but listen, <em>you got this</em>. <a href="https://piccalil.li/javascript-for-everyone">JavaScript for Everyone</a> is designed to take you inside JavaScript’s head. Once you’ve started seeing how the gears mesh &mdash; seen the fingerprints left behind by the people who built the language, and the good, bad, and sometimes baffling decisions that went into that &mdash; no <em>itera-</em>, whether <em>-ble</em> or <em>-tor</em> will be able to stand in your way.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://piccalil.li/javascript-for-everyone">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="450"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png"
			
			sizes="100vw"
			alt="Javascript for everyone course announcement"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      <a href='https://piccalil.li/javascript-for-everyone'>JavaScript for Everyone</a> is now available and the launch price runs until midnight, October 28. Save £60 off the full price of £249 and get it for £189! (<a href='https://files.smashing.media/articles/javascript-for-everyone-iterators/1-javascript-for-everyone.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>My goal is to teach you the <em>deep magic</em> &mdash; the <em>how</em> and the <em>why</em> of JavaScript, using the syntaxes you’re most likely to encounter in your day-to-day work, at your pace and on your terms. If you’re new to the language, you’ll walk away from this course with a foundational understanding of JavaScript worth hundreds of hours of trial-and-error. If you’re a junior developer, you’ll finish this course with a depth of knowledge to rival any senior.</p>

<p>I hope to see you there.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Fuqiao Xue</author><title>The Power Of The &lt;code>Intl&lt;/code> API: A Definitive Guide To Browser-Native Internationalization</title><link>https://www.smashingmagazine.com/2025/08/power-intl-api-guide-browser-native-internationalization/</link><pubDate>Fri, 08 Aug 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/08/power-intl-api-guide-browser-native-internationalization/</guid><description>Internationalization isn’t just translation. It’s about formatting dates, pluralizing words, sorting names, and more, all according to specific locales. Instead of relying on heavy third-party libraries, modern JavaScript offers the Intl API &amp;mdash; a powerful, native way to handle i18n. A quiet reminder that the web truly is worldwide.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/08/power-intl-api-guide-browser-native-internationalization/" />
              <title>The Power Of The &lt;code&gt;Intl&lt;/code&gt; API: A Definitive Guide To Browser-Native Internationalization</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>The Power Of The &lt;code&gt;Intl&lt;/code&gt; API: A Definitive Guide To Browser-Native Internationalization</h1>
                  
                    
                    <address>Fuqiao Xue</address>
                  
                  <time datetime="2025-08-08T10:00:00&#43;00:00" class="op-published">2025-08-08T10:00:00+00:00</time>
                  <time datetime="2025-08-08T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>It’s a common misconception that internationalization (i18n) is simply about translating text. While crucial, translation is merely one facet. One of the complexities lies in <strong>adapting information for diverse cultural expectations</strong>: How do you display a date in Japan versus Germany? What’s the correct way to pluralize an item in Arabic versus English? How do you sort a list of names in various languages?</p>

<p>Many developers have relied on weighty third-party libraries or, worse, custom-built formatting functions to tackle these challenges. These solutions, while functional, often come with significant overhead: increased bundle size, potential performance bottlenecks, and the constant struggle to keep up with evolving linguistic rules and locale data.</p>

<p>Enter the <strong>ECMAScript Internationalization API</strong>, more commonly known as the <code>Intl</code> object. This silent powerhouse, built directly into modern JavaScript environments, is an often-underestimated, yet incredibly <strong>potent, native, performant, and standards-compliant solution</strong> for handling data internationalization. It’s a testament to the web’s commitment to being <em>worldwide</em>, providing a unified and efficient way to format numbers, dates, lists, and more, according to specific locales.</p>

<h2 id="intl-and-locales-more-than-just-language-codes"><code>Intl</code> And Locales: More Than Just Language Codes</h2>

<p>At the heart of <code>Intl</code> lies the concept of a <strong>locale</strong>. A locale is far more than just a two-letter language code (like <code>en</code> for English or <code>es</code> for Spanish). It encapsulates the complete context needed to present information appropriately for a specific cultural group. This includes:</p>

<ul>
<li><strong>Language</strong>: The primary linguistic medium (e.g., <code>en</code>, <code>es</code>, <code>fr</code>).</li>
<li><strong>Script</strong>: The script (e.g., <code>Latn</code> for Latin, <code>Cyrl</code> for Cyrillic). For example, <code>zh-Hans</code> for Simplified Chinese, vs. <code>zh-Hant</code> for Traditional Chinese.</li>
<li><strong>Region</strong>: The geographic area (e.g., <code>US</code> for United States, <code>GB</code> for Great Britain, <code>DE</code> for Germany). This is crucial for variations within the same language, such as <code>en-US</code> vs. <code>en-GB</code>, which differ in date, time, and number formatting.</li>
<li><strong>Preferences/Variants</strong>: Further specific cultural or linguistic preferences. See <a href="https://www.w3.org/International/questions/qa-choosing-language-tags">“Choosing a Language Tag”</a> from W3C for more information.</li>
</ul>

<p>Typically, you’ll want to choose the locale according to the language of the web page. This can be determined from the <code>lang</code> attribute:</p>

<div class="break-out">
<pre><code class="language-javascript">// Get the page's language from the HTML lang attribute
const pageLocale = document.documentElement.lang || 'en-US'; // Fallback to 'en-US'
</code></pre>
</div>

<p>Occasionally, you may want to override the page locale with a specific locale, such as when displaying content in multiple languages:</p>

<div class="break-out">
<pre><code class="language-javascript">// Force a specific locale regardless of page language
const tutorialFormatter = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' });

console.log(`Chinese example: ${tutorialFormatter.format(199.99)}`); // Output: ¥199.99
</code></pre>
</div>

<p>In some cases, you might want to use the user’s preferred language:</p>

<div class="break-out">
<pre><code class="language-javascript">// Use the user's preferred language
const browserLocale = navigator.language || 'ja-JP';

const formatter = new Intl.NumberFormat(browserLocale, { style: 'currency', currency: 'JPY' });
</code></pre>
</div>

<p>When you instantiate an <code>Intl</code> formatter, you can optionally pass one or more locale strings. The API will then select the most appropriate locale based on availability and preference.</p>

<h2 id="core-formatting-powerhouses">Core Formatting Powerhouses</h2>

<p>The <code>Intl</code> object exposes several constructors, each for a specific formatting task. Let’s delve into the most frequently used ones, along with some powerful, often-overlooked gems.</p>

<h3 id="1-intl-datetimeformat-dates-and-times-globally">1. <code>Intl.DateTimeFormat</code>: Dates and Times, Globally</h3>

<p>Formatting dates and times is a classic i18n problem. Should it be MM/DD/YYYY or DD.MM.YYYY? Should the month be a number or a full word? <code>Intl.DateTimeFormat</code> handles all this with ease.</p>

<div class="break-out">
<pre><code class="language-javascript">const date = new Date(2025, 6, 27, 14, 30, 0); // June 27, 2025, 2:30 PM

// Specific locale and options (e.g., long date, short time)
const options = {
  weekday: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: 'numeric',
  minute: 'numeric',
  timeZoneName: 'shortOffset' // e.g., "GMT+8"
};

console.log(new Intl.DateTimeFormat('en-US', options).format(date));

// "Friday, June 27, 2025 at 2:30 PM GMT+8"
console.log(new Intl.DateTimeFormat('de-DE', options).format(date));

// "Freitag, 27. Juni 2025 um 14:30 GMT+8"

// Using dateStyle and timeStyle for common patterns
console.log(new Intl.DateTimeFormat('en-GB', { dateStyle: 'full', timeStyle: 'short' }).format(date));

// "Friday 27 June 2025 at 14:30"

console.log(new Intl.DateTimeFormat('ja-JP', { dateStyle: 'long', timeStyle: 'short' }).format(date));

// "2025年6月27日 14:30"
</code></pre>
</div>

<p>The flexibility of <code>options</code> for <code>DateTimeFormat</code> is vast, allowing control over year, month, day, weekday, hour, minute, second, time zone, and more.</p>

<h3 id="2-intl-numberformat-numbers-with-cultural-nuance">2. <code>Intl.NumberFormat</code>: Numbers With Cultural Nuance</h3>

<p>Beyond simple decimal places, numbers require careful handling: thousands separators, decimal markers, currency symbols, and percentage signs vary wildly across locales.</p>

<div class="break-out">
<pre><code class="language-javascript">const price = 123456.789;

// Currency formatting
console.log(new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price));

// "$123,456.79" (auto-rounds)

console.log(new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price));

// "123.456,79 €"

// Units
console.log(new Intl.NumberFormat('en-US', { style: 'unit', unit: 'meter', unitDisplay: 'long' }).format(100));

// "100 meters"

console.log(new Intl.NumberFormat('fr-FR', { style: 'unit', unit: 'kilogram', unitDisplay: 'short' }).format(5.5));

// "5,5 kg"
</code></pre>
</div>

<p>Options like <code>minimumFractionDigits</code>, <code>maximumFractionDigits</code>, and <code>notation</code> (e.g., <code>scientific</code>, <code>compact</code>) provide even finer control.</p>

<h3 id="3-intl-listformat-natural-language-lists">3. <code>Intl.ListFormat</code>: Natural Language Lists</h3>

<p>Presenting lists of items is surprisingly tricky. English uses “and” for conjunction and “or” for disjunction. Many languages have different conjunctions, and some require specific punctuation.</p>

<p>This API simplifies a task that would otherwise require complex conditional logic:</p>

<div class="break-out">
<pre><code class="language-javascript">const items = ['apples', 'oranges', 'bananas'];

// Conjunction ("and") list
console.log(new Intl.ListFormat('en-US', { type: 'conjunction' }).format(items));

// "apples, oranges, and bananas"

console.log(new Intl.ListFormat('de-DE', { type: 'conjunction' }).format(items));

// "Äpfel, Orangen und Bananen"

// Disjunction ("or") list
console.log(new Intl.ListFormat('en-US', { type: 'disjunction' }).format(items));

// "apples, oranges, or bananas"

console.log(new Intl.ListFormat('fr-FR', { type: 'disjunction' }).format(items));

// "apples, oranges ou bananas"
</code></pre>
</div>

<h3 id="4-intl-relativetimeformat-human-friendly-timestamps">4. <code>Intl.RelativeTimeFormat</code>: Human-Friendly Timestamps</h3>

<p>Displaying “2 days ago” or “in 3 months” is common in UI, but localizing these phrases accurately requires extensive data. <code>Intl.RelativeTimeFormat</code> automates this.</p>

<div class="break-out">
<pre><code class="language-javascript">const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });

console.log(rtf.format(-1, 'day'));    // "yesterday"
console.log(rtf.format(1, 'day'));     // "tomorrow"
console.log(rtf.format(-7, 'day'));    // "7 days ago"
console.log(rtf.format(3, 'month'));   // "in 3 months"
console.log(rtf.format(-2, 'year'));   // "2 years ago"

// French example:
const frRtf = new Intl.RelativeTimeFormat('fr-FR', { numeric: 'auto', style: 'long' });

console.log(frRtf.format(-1, 'day'));    // "hier"
console.log(frRtf.format(1, 'day'));     // "demain"
console.log(frRtf.format(-7, 'day'));    // "il y a 7 jours"
console.log(frRtf.format(3, 'month'));   // "dans 3 mois"
</code></pre>
</div>

<p>The <code>numeric: 'always'</code> option would force “1 day ago” instead of “yesterday”.</p>

<h3 id="5-intl-pluralrules-mastering-pluralization">5. <code>Intl.PluralRules</code>: Mastering Pluralization</h3>

<p>This is arguably one of the most critical aspects of i18n. Different languages have vastly different pluralization rules (e.g., English has singular/plural, Arabic has zero, one, two, many&hellip;). <code>Intl.PluralRules</code> allows you to determine the “plural category” for a given number in a specific locale.</p>

<pre><code class="language-javascript">const prEn = new Intl.PluralRules('en-US');

console.log(prEn.select(0));    // "other" (for "0 items")
console.log(prEn.select(1));    // "one"   (for "1 item")
console.log(prEn.select(2));    // "other" (for "2 items")

const prAr = new Intl.PluralRules('ar-EG');

console.log(prAr.select(0));    // "zero"
console.log(prAr.select(1));    // "one"
console.log(prAr.select(2));    // "two"
console.log(prAr.select(10));   // "few"
console.log(prAr.select(100));  // "other"
</code></pre>

<p>This API doesn’t pluralize text directly, but it provides the essential classification needed to select the correct translation string from your message bundles. For example, if you have message keys like <code>item.one</code>, <code>item.other</code>, you’d use <code>pr.select(count)</code> to pick the right one.</p>

<h3 id="6-intl-displaynames-localized-names-for-everything">6. <code>Intl.DisplayNames</code>: Localized Names For Everything</h3>

<p>Need to display the name of a language, a region, or a script in the user’s preferred language? <code>Intl.DisplayNames</code> is your comprehensive solution.</p>

<div class="break-out">
<pre><code class="language-javascript">// Display language names in English
const langNamesEn = new Intl.DisplayNames(['en'], { type: 'language' });

console.log(langNamesEn.of('fr'));      // "French"
console.log(langNamesEn.of('es-MX'));   // "Mexican Spanish"

// Display language names in French
const langNamesFr = new Intl.DisplayNames(['fr'], { type: 'language' });

console.log(langNamesFr.of('en'));      // "anglais"
console.log(langNamesFr.of('zh-Hans')); // "chinois (simplifié)"

// Display region names
const regionNamesEn = new Intl.DisplayNames(['en'], { type: 'region' });

console.log(regionNamesEn.of('US'));    // "United States"
console.log(regionNamesEn.of('DE'));    // "Germany"

// Display script names
const scriptNamesEn = new Intl.DisplayNames(['en'], { type: 'script' });

console.log(scriptNamesEn.of('Latn'));  // "Latin"
console.log(scriptNamesEn.of('Arab'));  // "Arabic"
</code></pre>
</div>

<p>With <code>Intl.DisplayNames</code>, you avoid hardcoding countless mappings for language names, regions, or scripts, keeping your application robust and lean.</p>

<h2 id="browser-support">Browser Support</h2>

<p>You might be wondering about browser compatibility. The good news is that <code>Intl</code> has excellent support across modern browsers. All major browsers (Chrome, Firefox, Safari, Edge) fully support the core functionality discussed (<code>DateTimeFormat</code>, <code>NumberFormat</code>, <code>ListFormat</code>, <code>RelativeTimeFormat</code>, <code>PluralRules</code>, <code>DisplayNames</code>). You can confidently use these APIs without polyfills for the majority of your user base.</p>

<h2 id="conclusion-embrace-the-global-web-with-intl">Conclusion: Embrace The Global Web With <code>Intl</code></h2>

<p>The <code>Intl</code> API is a cornerstone of modern web development for a global audience. It empowers front-end developers to deliver <strong>highly localized user experiences</strong> with minimal effort, leveraging the browser’s built-in, optimized capabilities.</p>

<p>By adopting <code>Intl</code>, you <strong>reduce dependencies</strong>, <strong>shrink bundle sizes</strong>, and <strong>improve performance</strong>, all while ensuring your application respects and adapts to the diverse linguistic and cultural expectations of users worldwide. Stop wrestling with custom formatting logic and embrace this standards-compliant tool!</p>

<p>It’s important to remember that <code>Intl</code> handles the <em>formatting</em> of data. While incredibly powerful, it doesn’t solve every aspect of internationalization. Content translation, bidirectional text (RTL/LTR), locale-specific typography, and deep cultural nuances beyond data formatting still require careful consideration. (I may write about these in the future!) However, for presenting dynamic data accurately and intuitively, <code>Intl</code> is the browser-native answer.</p>

<h3 id="further-reading-resources">Further Reading &amp; Resources</h3>

<ul>
<li>MDN Web Docs:

<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl"><code>Intl namespace object</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat"><code>Intl.DateTimeFormat</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat"><code>Intl.NumberFormat</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat"><code>Intl.ListFormat</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat"><code>Intl.RelativeTimeFormat</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules"><code>Intl.PluralRules</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames"><code>Intl.DisplayNames</code></a></li>
</ul></li>
<li>ECMAScript Internationalization API Specification: <a href="https://tc39.es/ecma402/">The official ECMA-402 Standard</a></li>
<li><a href="https://www.w3.org/International/questions/qa-choosing-language-tags">Choosing a Language Tag</a></li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Amejimaobari Ollornwi</author><title>Handling JavaScript Event Listeners With Parameters</title><link>https://www.smashingmagazine.com/2025/07/handling-javascript-event-listeners-parameters/</link><pubDate>Mon, 21 Jul 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/07/handling-javascript-event-listeners-parameters/</guid><description>Event listeners are essential for interactivity in JavaScript, but they can quietly cause memory leaks if not removed properly. And what if your event listener needs parameters? That’s where things get interesting. Amejimaobari Ollornwi shares which JavaScript features make handling parameters with event handlers both possible and well-supported.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/07/handling-javascript-event-listeners-parameters/" />
              <title>Handling JavaScript Event Listeners With Parameters</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Handling JavaScript Event Listeners With Parameters</h1>
                  
                    
                    <address>Amejimaobari Ollornwi</address>
                  
                  <time datetime="2025-07-21T10:00:00&#43;00:00" class="op-published">2025-07-21T10:00:00+00:00</time>
                  <time datetime="2025-07-21T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>JavaScript event listeners are very important, as they exist in almost every web application that requires interactivity. As common as they are, it is also essential for them to be managed properly. Improperly managed event listeners can lead to memory leaks and can sometimes cause performance issues in extreme cases.</p>

<p>Here’s the real problem: <strong>JavaScript event listeners are often not removed after they are added.</strong> And when they are added, they do not require parameters most of the time &mdash; except in rare cases, which makes them a little trickier to handle.</p>

<p>A common scenario where you may need to use parameters with event handlers is when you have a dynamic list of tasks, where each task in the list has a “Delete” button attached to an event handler that uses the task’s ID as a parameter to remove the task. In a situation like this, it is a good idea to remove the event listener once the task has been completed to ensure that the deleted element can be successfully cleaned up, a process known as <a href="https://javascript.info/garbage-collection">garbage collecti</a><a href="https://javascript.info/garbage-collection">on</a>.</p>

<h2 id="a-common-mistake-when-adding-event-listeners">A Common Mistake When Adding Event Listeners</h2>

<p>A very common mistake when adding parameters to event handlers is calling the function with its parameters inside the <code>addEventListener()</code> method. This is what I mean:</p>

<pre><code class="language-javascript">button.addEventListener('click', myFunction(param1, param2));
</code></pre>

<p>The browser responds to this line by immediately calling the function, irrespective of whether or not the click event has happened. In other words, the function is invoked right away instead of being deferred, so it never fires when the click event actually occurs.</p>

<p>You may also receive the following console error in some cases:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="75"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png"
			
			sizes="100vw"
			alt="Uncaught TypeError"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Uncaught TypeError: Failed to execute. <code>addEventListener</code> on <code>EventTarget</code>: parameter is not of type <code>Object</code>. (<a href='https://files.smashing.media/articles/handling-javascript-event-listeners-parameters/1-uncaught-typeerror.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>This error makes sense because the second parameter of the <code>addEventListener</code> method <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#listener">can only accept</a> a JavaScript function, an object with a <code>handleEvent()</code> method, or simply <code>null</code>. A quick and easy way to avoid this error is by changing the second parameter of the <code>addEventListener</code> method to an arrow or anonymous function.</p>

<pre><code class="language-javascript">button.addEventListener('click', (event) =&gt; {
  myFunction(event, param1, param2); // Runs on click
});
</code></pre>

<p>The only hiccup with using arrow and anonymous functions is that they cannot be removed with the traditional <code>removeEventListener()</code> method; you will have to make use of <code>AbortController</code>, which may be overkill for simple cases. <code>AbortController</code> shines when you have multiple event listeners to remove at once.</p>

<p>For simple cases where you have just one or two event listeners to remove, the <code>removeEventListener()</code> method still proves useful. However, in order to make use of it, you’ll need to store your function as a reference to the listener.</p>

<h2 id="using-parameters-with-event-handlers">Using Parameters With Event Handlers</h2>

<p>There are several ways to include parameters with event handlers. However, for the purpose of this demonstration, we are going to constrain our focus to the following two:</p>

<h3 id="option-1-arrow-and-anonymous-functions">Option 1: Arrow And Anonymous Functions</h3>

<p>Using arrow and anonymous functions is the fastest and easiest way to get the job done.</p>

<p>To add an event handler with parameters using arrow and anonymous functions, we’ll first need to call the function we’re going to create inside the arrow function attached to the event listener:</p>

<pre><code class="language-javascript">const button = document.querySelector("#myButton");

button.addEventListener("click", (event) =&gt; {
  handleClick(event, "hello", "world");
});
</code></pre>

<p>After that, we can create the function with parameters:</p>

<pre><code class="language-javascript">function handleClick(event, param1, param2) {
  console.log(param1, param2, event.type, event.target);
}
</code></pre>

<p>Note that with this method, removing the event listener requires the <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController"><code>AbortController</code></a>. To remove the event listener, we create a new <code>AbortController</code> object and then retrieve the <code>AbortSignal</code> object from it:</p>

<pre><code class="language-javascript">const controller = new AbortController();
const { signal } = controller;
</code></pre>

<p>Next, we can pass the <code>signal</code> from the <code>controller</code> as an option in the <code>removeEventListener()</code> method:</p>

<pre><code class="language-javascript">button.addEventListener("click", (event) =&gt; {
  handleClick(event, "hello", "world");
}, { signal });
</code></pre>

<p>Now we can remove the event listener by calling <code>AbortController.abort()</code>:</p>

<pre><code class="language-javascript">controller.abort()
</code></pre>

<h3 id="option-2-closures">Option 2: Closures</h3>

<p>Closures in JavaScript are another feature that can help us with event handlers. Remember the mistake that produced a type error? That mistake can also be corrected with closures. Specifically, with closures, a function can access variables from its outer scope.</p>

<p>In other words, we can access the parameters we need in the event handler from the outer function:</p>

<div class="break-out">
<pre><code class="language-javascript">function createHandler(message, number) {
  // Event handler
  return function (event) {
  console.log(`${message} ${number} - Clicked element:`, event.target);
    };
  }

  const button = document.querySelector("&#35;myButton");
  button.addEventListener("click", createHandler("Hello, world!", 1));
}
</code></pre>
</div>

<p>This establishes a function that returns another function. The function that is created is then called as the second parameter in the <code>addEventListener()</code> method so that the inner function is returned as the event handler. And with the power of closures, the parameters from the outer function will be made available for use in the inner function.</p>

<p>Notice how the <code>event</code> object is made available to the inner function. This is because the inner function is what is being attached as the event handler. The event object is passed to the function automatically because it’s the event handler.</p>

<p>To remove the event listener, we can use the <code>AbortController</code> like we did before. However, this time, let’s see how we can do that using the <code>removeEventListener()</code> method instead.</p>

<p>In order for the <code>removeEventListener</code> method to work, a reference to the <code>createHandler</code> function needs to be stored and used in the <code>addEventListener</code> method:</p>

<div class="break-out">
<pre><code class="language-javascript">function createHandler(message, number) {
  return function (event) {
    console.log(`${message} ${number} - Clicked element:`, event.target);
  };
}
const handler = createHandler("Hello, world!", 1);
button.addEventListener("click", handler);
</code></pre>
</div>

<p>Now, the event listener can be removed like this:</p>

<pre><code class="language-javascript">button.removeEventListener("click", handler);
</code></pre>

<h2 id="conclusion">Conclusion</h2>

<p>It is good practice to always remove event listeners whenever they are no longer needed to prevent memory leaks. Most times, event handlers do not require parameters; however, in rare cases, they do. Using JavaScript features like closures, <code>AbortController</code>, and <code>removeEventListener</code>, handling parameters with event handlers is both possible and well-supported.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Blake Lundquist</author><title>Creating The &amp;ldquo;Moving Highlight&amp;rdquo; Navigation Bar With JavaScript And CSS</title><link>https://www.smashingmagazine.com/2025/06/creating-moving-highlight-navigation-bar-javascript-css/</link><pubDate>Wed, 11 Jun 2025 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/06/creating-moving-highlight-navigation-bar-javascript-css/</guid><description>In this tutorial, Blake Lundquist walks us through two methods of creating the “moving-highlight” navigation pattern using only plain JavaScript and CSS. The first technique uses the &lt;code>getBoundingClientRect&lt;/code> method to explicitly animate the border between navigation bar items when they are clicked. The second approach achieves the same functionality using the new View Transition API.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/06/creating-moving-highlight-navigation-bar-javascript-css/" />
              <title>Creating The &amp;ldquo;Moving Highlight&amp;rdquo; Navigation Bar With JavaScript And CSS</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Creating The &amp;ldquo;Moving Highlight&amp;rdquo; Navigation Bar With JavaScript And CSS</h1>
                  
                    
                    <address>Blake Lundquist</address>
                  
                  <time datetime="2025-06-11T13:00:00&#43;00:00" class="op-published">2025-06-11T13:00:00+00:00</time>
                  <time datetime="2025-06-11T13:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>I recently came across an old jQuery tutorial demonstrating a <strong>“moving highlight” navigation bar</strong> and decided the concept was due for a modern upgrade. With this pattern, the border around the active navigation item animates directly from one element to another as the user clicks on menu items. In 2025, we have much better tools to manipulate the DOM via vanilla JavaScript. New features like the <a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API">View Transition API</a> make progressive enhancement more easily achievable and handle a lot of the animation minutiae.</p>

<figure><a href="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/1-moving-highlight-navigation-bar.gif"><img src="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/1-moving-highlight-navigation-bar.gif" width="800" height="131" alt="An example of a “moving highlight” navigation bar" /></a><figcaption>(<a href="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/1-moving-highlight-navigation-bar.gif">Large preview</a>)</figcaption></figure>

<p>In this tutorial, I will demonstrate two methods of creating the “moving highlight” navigation bar using plain JavaScript and CSS. The first example uses the <code>getBoundingClientRect</code> method to explicitly animate the border between navigation bar items when they are clicked. The second example achieves the same functionality using the new View Transition API.</p>

<h2 id="the-initial-markup">The Initial Markup</h2>

<p>Let’s assume that we have a single-page application where content changes without the page being reloaded. The starting HTML and CSS are your standard navigation bar with an additional <code>div</code> element containing an <code>id</code> of <code>#highlight</code>. We give the first navigation item a class of <code>.active</code>.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="EajQyBW"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Moving Highlight Navbar Starting Markup [forked]](https://codepen.io/smashingmag/pen/EajQyBW) by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/EajQyBW">Moving Highlight Navbar Starting Markup [forked]</a> by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</figcaption>
</figure>

<p>For this version, we will position the <code>#highlight</code> element around the element with the <code>.active</code> class to create a border. We can utilize <code>absolute</code> positioning and animate the element across the navigation bar to create the desired effect. We’ll hide it off-screen initially by adding <code>left: -200px</code> and include <code>transition</code> styles for all properties so that any changes in the position and size of the element will happen gradually.</p>

<pre><code class="language-css">&#35;highlight {
  z-index: 0;
  position: absolute;
  height: 100%;
  width: 100px;
  left: -200px;
  border: 2px solid green;
  box-sizing: border-box;
  transition: all 0.2s ease;
}
</code></pre>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="/printed-books/touch-design-for-mobile-interfaces/">Touch Design for Mobile Interfaces</a></strong>, Steven Hoober’s brand-new guide on <strong>designing for mobile</strong> with proven, universal, human-centric guidelines. <strong>400 pages</strong>, jam-packed with in-depth user research and <strong>best practices</strong>.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/touch-design-for-mobile-interfaces/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://www.smashingmagazine.com/printed-books/touch-design-for-mobile-interfaces/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/14bcab88-b622-47f6-a51d-76b0aa003597/touch-design-book-shop-opt.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/b14658fc-bb2d-41a6-8d1a-70eaaf1b8ec8/touch-design-book-shop-opt.png"
    alt="Feature Panel"
    width="480"
    height="697"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="add-a-boilerplate-event-handler-for-click-interactions">Add A Boilerplate Event Handler For Click Interactions</h2>

<p>We want the highlight element to animate when a user changes the <code>.active</code> navigation item. Let’s add a <code>click</code> event handler to the <code>nav</code> element, then filter for events caused only by elements matching our desired selector. In this case, we only want to change the <code>.active</code> nav item if the user clicks on a link that does not already have the <code>.active</code> class.</p>

<p>Initially, we can call <code>console.log</code> to ensure the handler fires only when expected:</p>

<div class="break-out">
<pre><code class="language-javascript">const navbar = document.querySelector('nav');

navbar.addEventListener('click', function (event) {
  // return if the clicked element doesn't have the correct selector
  if (!event.target.matches('nav a:not(active)')) {
    return;
  }
  
  console.log('click');
});
</code></pre>
</div>

<p>Open your browser console and try clicking different items in the navigation bar. You should only see <code>&quot;click&quot;</code> being logged when you select a new item in the navigation bar.</p>

<p>Now that we know our event handler is working on the correct elements let’s add code to move the <code>.active</code> class to the navigation item that was clicked. We can use the object passed into the event handler to find the element that initialized the event and give that element a class of <code>.active</code> after removing it from the previously active item.</p>

<div class="break-out">
<pre><code class="language-javascript">const navbar = document.querySelector('nav');

navbar.addEventListener('click', function (event) {
  // return if the clicked element doesn't have the correct selector
  if (!event.target.matches('nav a:not(active)')) {
    return;
  }
  
-  console.log('click');
+  document.querySelector('nav a.active').classList.remove('active');
+  event.target.classList.add('active');
  
});
</code></pre>
</div>

<p>Our <code>#highlight</code> element needs to move across the navigation bar and position itself around the active item. Let’s write a function to calculate a new position and width. Since the <code>#highlight</code> selector has <code>transition</code> styles applied, it will move gradually when its position changes.</p>

<p>Using <code>getBoundingClientRect</code>, we can get information about the position and size of an element. We calculate the width of the active navigation item and its offset from the left boundary of the parent element. Then, we assign styles to the highlight element so that its size and position match.</p>

<div class="break-out">
<pre><code class="language-javascript">// handler for moving the highlight
const moveHighlight = () =&gt; {
  const activeNavItem = document.querySelector('a.active');
  const highlighterElement = document.querySelector('#highlight');
  
  const width = activeNavItem.offsetWidth;

  const itemPos = activeNavItem.getBoundingClientRect();
  const navbarPos = navbar.getBoundingClientRect()
  const relativePosX = itemPos.left - navbarPos.left;

  const styles = {
    left: `${relativePosX}px`,
    width: `${width}px`,
  };

  Object.assign(highlighterElement.style, styles);
}
</code></pre>
</div>

<p>Let’s call our new function when the click event fires:</p>

<div class="break-out">
<pre><code class="language-javascript">navbar.addEventListener('click', function (event) {
  // return if the clicked element doesn't have the correct selector
  if (!event.target.matches('nav a:not(active)')) {
    return;
  }
  
  document.querySelector('nav a.active').classList.remove('active');
  event.target.classList.add('active');
  
+  moveHighlight();
});
</code></pre>
</div>

<p>Finally, let’s also call the function immediately so that the border moves behind our initial active item when the page first loads:</p>

<pre><code class="language-javascript">// handler for moving the highlight
const moveHighlight = () =&gt; {
 // ...
}

// display the highlight when the page loads
moveHighlight();
</code></pre>

<p>Now, the border moves across the navigation bar when a new item is selected. Try clicking the different navigation links to animate the navigation bar.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="WbvMxqV"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Moving Highlight Navbar [forked]](https://codepen.io/smashingmag/pen/WbvMxqV) by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/WbvMxqV">Moving Highlight Navbar [forked]</a> by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</figcaption>
</figure>

<p>That only took a few lines of vanilla JavaScript and could easily be extended to account for other interactions, like <code>mouseover</code> events. In the next section, we will explore refactoring this feature using the View Transition API.</p>

<div class="partners__lead-place"></div>

<h2 id="using-the-view-transition-api">Using The View Transition API</h2>

<p>The View Transition API provides functionality to create animated transitions between website views. Under the hood, the API creates snapshots of “before” and “after” views and then handles transitioning between them. View transitions are useful for creating animations between documents, providing the <strong>native-app-like user experience</strong> featured in frameworks like <a href="https://docs.astro.build/en/guides/view-transitions/">Astro</a>. However, the API also provides handlers meant for <strong>SPA-style applications</strong>. We will use it to reduce the JavaScript needed in our implementation and more easily create fallback functionality.</p>

<p>For this approach, we no longer need a separate <code>#highlight</code> element. Instead, we can style the <code>.active</code> navigation item directly using pseudo-selectors and let the View Transition API handle the animation between the before-and-after UI states when a new navigation item is clicked.</p>

<p>We’ll start by getting rid of the <code>#highlight</code> element and its associated CSS and replacing it with styles for the <code>nav a::after</code> pseudo-selector:</p>

<pre><code class="language-html">&lt;nav&gt;
  - &lt;div id="highlight"&gt;&lt;/div&gt;
  &lt;a href="#" class="active"&gt;Home&lt;/a&gt;
  &lt;a href="#services"&gt;Services&lt;/a&gt;
  &lt;a href="#about"&gt;About&lt;/a&gt;
  &lt;a href="#contact"&gt;Contact&lt;/a&gt;
&lt;/nav&gt;
</code></pre>

<pre><code class="language-css">- &#35;highlight {
-  z-index: 0;
-  position: absolute;
-  height: 100%;
-  width: 0;
-  left: 0;
-  box-sizing: border-box;
-  transition: all 0.2s ease;
- }

+ nav a::after {
+  content: " ";
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  border: none;
+  box-sizing: border-box;
+ }
</code></pre>

<p>For the <code>.active</code> class, we include the <code>view-transition-name</code> property, thus unlocking the magic of the View Transition API. Once we trigger the view transition and change the location of the <code>.active</code> navigation item in the DOM, “before” and “after” snapshots will be taken, and the browser will animate the border across the bar. We’ll give our view transition the name of <code>highlight</code>, but we could theoretically give it any name.</p>

<pre><code class="language-css">nav a.active::after {
  border: 2px solid green;
  view-transition-name: highlight;
}
</code></pre>

<p>Once we have a selector that contains a <code>view-transition-name</code> property, the only remaining step is to trigger the transition using the <code>startViewTransition</code> method and pass in a callback function.</p>

<div class="break-out">
<pre><code class="language-javascript">const navbar = document.querySelector('nav');

// Change the active nav item on click
navbar.addEventListener('click', async  function (event) {

  if (!event.target.matches('nav a:not(.active)')) {
    return;
  }
  
  document.startViewTransition(() =&gt; {
    document.querySelector('nav a.active').classList.remove('active');

    event.target.classList.add('active');
  });
});
</code></pre>
</div>

<p>Above is a revised version of the <code>click</code> handler. Instead of doing all the calculations for the size and position of the moving border ourselves, the View Transition API handles all of it for us. We only need to call <code>document.startViewTransition</code> and pass in a callback function to change the item that has the <code>.active</code> class!</p>

<div class="partners__lead-place"></div>

<h2 id="adjusting-the-view-transition">Adjusting The View Transition</h2>

<p>At this point, when clicking on a navigation link, you’ll notice that the transition works, but some strange sizing issues are visible.</p>

<figure><a href="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/2-view-transition-sizing-issues.gif"><img src="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/2-view-transition-sizing-issues-800px.gif" width="800" height="163" alt="The view transition with sizing issues" /></a><figcaption>(<a href="https://files.smashing.media/articles/creating-moving-highlight-navigation-bar-vanilla-javascript/2-view-transition-sizing-issues.gif">Large preview</a>)</figcaption></figure>

<p>This sizing inconsistency is caused by aspect ratio changes during the course of the view transition. We won’t go into detail here, but <a href="https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/">Jake Archibald has a detailed explanation you can read</a> for more information. In short, to ensure the height of the border stays uniform throughout the transition, we need to declare an explicit <code>height</code> for the <code>::view-transition-old</code> and <code>::view-transition-new</code> pseudo-selectors representing a static snapshot of the old and new view, respectively.</p>

<pre><code class="language-css">::view-transition-old(highlight) {
  height: 100%;
}

::view-transition-new(highlight) {
  height: 100%;
}
</code></pre>

<p>Let’s do some final refactoring to tidy up our code by moving the callback to a separate function and adding a fallback for when view transitions aren’t supported:</p>

<div class="break-out">
<pre><code class="language-javascript">const navbar = document.querySelector('nav');

// change the item that has the .active class applied
const setActiveElement = (elem) =&gt; {
  document.querySelector('nav a.active').classList.remove('active');
  elem.classList.add('active');
}

// Start view transition and pass in a callback on click
navbar.addEventListener('click', async  function (event) {
  if (!event.target.matches('nav a:not(.active)')) {
    return;
  }

  // Fallback for browsers that don't support View Transitions:
  if (!document.startViewTransition) {
    setActiveElement(event.target);
    return;
  }
  
  document.startViewTransition(() =&gt; setActiveElement(event.target));
});
</code></pre>
</div>

<p>Here’s our view transition-powered navigation bar! Observe the smooth transition when you click on the different links.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="ogXELKE"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Moving Highlight Navbar with View Transition [forked]](https://codepen.io/smashingmag/pen/ogXELKE) by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/ogXELKE">Moving Highlight Navbar with View Transition [forked]</a> by <a href="https://codepen.io/blakeeric">Blake Lundquist</a>.</figcaption>
</figure>

<h2 id="conclusion">Conclusion</h2>

<p>Animations and transitions between website UI states used to require many kilobytes of external libraries, along with verbose, confusing, and error-prone code, but vanilla JavaScript and CSS have since incorporated features to achieve <strong>native-app-like interactions without breaking the bank</strong>. We demonstrated this by implementing the “moving highlight” navigation pattern using two approaches: CSS transitions combined with the <code>getBoundingClientRect()</code> method and the View Transition API.</p>

<h3 id="resources">Resources</h3>

<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect"><code>getBoundingClientRect()</code> method documentation</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API">View Transition API documentation</a></li>
<li>“<a href="https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/">View Transitions: Handling Aspect Ratio Changes</a>” by Jake Archibald</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Amejimaobari Ollornwi</author><title>Building An Offline-Friendly Image Upload System</title><link>https://www.smashingmagazine.com/2025/04/building-offline-friendly-image-upload-system/</link><pubDate>Wed, 23 Apr 2025 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2025/04/building-offline-friendly-image-upload-system/</guid><description>Poor internet connectivity doesn’t have to mean poor UX. With PWA technologies like &lt;code>IndexedDB&lt;/code>, service workers, and the Background Sync API, you can build an offline-friendly image upload system that queues uploads and retries them automatically — so your users can upload stress-free, even when offline.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2025/04/building-offline-friendly-image-upload-system/" />
              <title>Building An Offline-Friendly Image Upload System</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Building An Offline-Friendly Image Upload System</h1>
                  
                    
                    <address>Amejimaobari Ollornwi</address>
                  
                  <time datetime="2025-04-23T10:00:00&#43;00:00" class="op-published">2025-04-23T10:00:00+00:00</time>
                  <time datetime="2025-04-23T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>So, you’re filling out an online form, and it asks you to upload a file. You click the input, select a file from your desktop, and are good to go. But something happens. The network drops, the file disappears, and you’re stuck having to re-upload the file. <strong>Poor network connectivity</strong> can lead you to spend an unreasonable amount of time trying to upload files successfully.</p>

<p>What ruins the user experience stems from having to constantly check network stability and retry the upload several times. While we may not be able to do much about network connectivity, as developers, we can always do something to ease the pain that comes with this problem.</p>

<p>One of the ways we can solve this problem is by tweaking image upload systems in a way that enables users to upload images offline &mdash; <strong>eliminating the need for a reliable network connection</strong>, and then having the system retry the upload process when the network becomes stable, without the user intervening.</p>

<p>This article is going to focus on explaining how to build <strong>an offline-friendly image upload system</strong> using PWA (progressive web application) technologies such as <code>IndexedDB</code>, service workers, and the Background Sync API. We will also briefly cover tips for improving the user experience for this system.</p>

<h2 id="planning-the-offline-image-upload-system">Planning The Offline Image Upload System</h2>

<p>Here’s a flow chart for an offline-friendly image upload system.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/building-offline-friendly-image-upload-system/1-upload-system-flow-chart.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="678"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/building-offline-friendly-image-upload-system/1-upload-system-flow-chart.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/building-offline-friendly-image-upload-system/1-upload-system-flow-chart.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/building-offline-friendly-image-upload-system/1-upload-system-flow-chart.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/building-offline-friendly-image-upload-system/1-upload-system-flow-chart.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/building-offline-friendly-image-upload-system/1-upload-system-flow-chart.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/building-offline-friendly-image-upload-system/1-upload-system-flow-chart.png"
			
			sizes="100vw"
			alt="Flow chart of an offline-friendly image upload system"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Flow chart of an offline-friendly image upload system (<a href='https://files.smashing.media/articles/building-offline-friendly-image-upload-system/1-upload-system-flow-chart.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>As shown in the flow chart, the process unfolds as follows:</p>

<ol>
<li><strong>The user selects an image.</strong><br />
The process begins by letting the user select their image.</li>
<li><strong>The image is stored locally in <code>IndexedDB</code>.</strong><br />
Next, the system checks for network connectivity. If network connectivity is available, the system uploads the image directly, avoiding unnecessary local storage usage. However, if the network is not available, the image will be stored in <code>IndexedDB</code>.</li>
<li><strong>The service worker detects when the network is restored.</strong><br />
With the image stored in <code>IndexedDB</code>, the system waits to detect when the network connection is restored to continue with the next step.</li>
<li><strong>The background sync processes pending uploads.</strong><br />
The moment the connection is restored, the system will try to upload the image again.</li>
<li><strong>The file is successfully uploaded</strong>.<br />
The moment the image is uploaded, the system will remove the local copy stored in <code>IndexedDB</code>.

<br /></li>
</ol>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p><p>Meet <a data-instant href="/the-smashing-newsletter/"><strong>Smashing Email Newsletter</strong></a> with useful tips on front-end, design &amp; UX. Subscribe and <strong>get “Smart Interface Design Checklists”</strong> &mdash; a <strong>free PDF deck</strong> with 150+ questions to ask yourself when designing and building almost <em>anything</em>.</p><div><section class="nlbf"><form action="//smashingmagazine.us1.list-manage.com/subscribe/post?u=16b832d9ad4b28edf261f34df&amp;id=a1666656e0" method="post"><div class="nlbwrapper"><label for="mce-EMAIL-hp" class="sr-only">Your (smashing) email</label><div class="nlbgroup"><input type="email" name="EMAIL" class="nlbf-email" id="mce-EMAIL-hp" placeholder="Your email">
<input type="submit" value="Meow!" name="subscribe" class="nlbf-button"></div></div></form><style>.c-garfield-the-cat .nlbwrapper{margin-bottom: 0;}.nlbf{display:flex;padding-bottom:.25em;padding-top:.5em;text-align:center;letter-spacing:-.5px;color:#fff;font-size:1.15em}.nlbgroup:hover{box-shadow:0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025)}.nlbf .nlbf-button,.nlbf .nlbf-email{flex-grow:1;flex-shrink:0;width:auto;margin:0;padding:.75em 1em;border:0;border-radius:11px;background:#fff;font-size:1em;box-shadow:none}.promo-box .nlbf-button:focus,.promo-box input.nlbf-email:active,.promo-box input.nlbf-email:focus{box-shadow:none}.nlbf-button:-ms-input-placeholder,.nlbf-email:-ms-input-placeholder{color:#777;font-style:italic}.nlbf-button::-webkit-input-placeholder,.nlbf-email::-webkit-input-placeholder{color:#777;font-style:italic}.nlbf-button:-ms-input-placeholder,.nlbf-button::-moz-placeholder,.nlbf-button::placeholder,.nlbf-email:-ms-input-placeholder,.nlbf-email::-moz-placeholder,.nlbf-email::placeholder{color:#777;font-style:italic}.nlbf .nlbf-button{transition:all .2s ease-in-out;color:#fff;background-color:#0168b8;font-weight:700;box-shadow:0 1px 1px rgba(0,0,0,.3);width:100%;border:0;border-left:1px solid #ddd;flex:2;border-top-left-radius:0;border-bottom-left-radius:0}.nlbf .nlbf-email{border-top-right-radius:0;border-bottom-right-radius:0;width:100%;flex:4;min-width:150px}@media all and (max-width:650px){.nlbf .nlbgroup{flex-wrap:wrap;box-shadow:none}.nlbf .nlbf-button,.nlbf .nlbf-email{border-radius:11px;border-left:none}.nlbf .nlbf-email{box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);min-width:100%}.nlbf .nlbf-button{margin-top:1em;box-shadow:0 1px 1px rgba(0,0,0,.5)}}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus,.nlbf .nlbf-button:hover{cursor:pointer;color:#fff;background-color:#0168b8;border-color:#dadada;box-shadow:0 1px 1px rgba(0,0,0,.3)}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus{outline:0!important;text-shadow:1px 1px 1px rgba(0,0,0,.3);box-shadow:inset 0 3px 3px rgba(0,0,0,.3)}.nlbgroup{display:flex;box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);border-radius:11px;transition:box-shadow .2s ease-in-out}.nlbwrapper{display:flex;flex-direction:column;justify-content:center}.nlbf form{width:100%}.nlbf .nlbgroup{margin:0}.nlbcaption{font-size:.9em;line-height:1.5em;color:#fff;border-radius:11px;padding:.5em 1em;display:inline-block;background-color:#0067b859;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.wf-loaded-stage2 .nlbf .nlbf-button{font-family:Mija}.mts{margin-top: 5px !important;}.mbn{margin-bottom: 0 !important;}</style></section><p class="mts mbn"><small class="promo-box__footer mtm block grey"><em>Once a week. Useful tips on <a href="https://www.smashingmagazine.com/the-smashing-newsletter/">front-end &amp; UX</a>. Trusted by 207.000 friendly folks.</em></small></p></div></p>
</div>
</div>
<div class="feature-panel-right-col">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-firechat.svg"
    alt="Feature Panel"
    width="310"
    height="400"
/>

</div>

<p></div>
</aside>
</div></p>

<h2 id="implementing-the-system">Implementing The System</h2>

<p>The first step in the system implementation is allowing the user to select their images. There are different ways you can achieve this:</p>

<ul>
<li>You can use a simple <code>&lt;input type=&quot;file&quot;&gt;</code> element;</li>
<li>A drag-and-drop interface.</li>
</ul>

<p>I would advise that you use both. Some users prefer to use the drag-and-drop interface, while others think the only way to upload images is through the <code>&lt;input type=&quot;file&quot;&gt;</code> element. Having both options will help improve the user experience. You can also consider allowing users to paste images directly in the browser using the Clipboard API.</p>

<h3 id="registering-the-service-worker">Registering The Service Worker</h3>

<p>At the heart of this solution is the <a href="https://www.smashingmagazine.com/2016/02/making-a-service-worker/">service worker</a>. Our service worker is going to be responsible for retrieving the image from the <code>IndexedDB</code> store, uploading it when the internet connection is restored, and clearing the <code>IndexedDB</code> store when the image has been uploaded.</p>

<p>To use a service worker, you first have to register one:</p>

<div class="break-out">
<pre><code class="language-javascript">if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
    .then(reg =&gt; console.log('Service Worker registered', reg))
    .catch(err =&gt; console.error('Service Worker registration failed', err));
}
</code></pre>
</div>

<h3 id="checking-for-network-connectivity">Checking For Network Connectivity</h3>

<p>Remember, the problem we are trying to solve is caused by <strong>unreliable network connectivity</strong>. If this problem does not exist, there is no point in trying to solve anything. Therefore, once the image is selected, we need to check if the user has a reliable internet connection before registering a sync event and storing the image in <code>IndexedDB</code>.</p>

<pre><code class="language-javascript">function uploadImage() {
  if (navigator.onLine) {
    // Upload Image
  } else {
    // register Sync Event
    // Store Images in IndexedDB
  }
}
</code></pre>

<p><strong>Note</strong>: I’m only using the <code>navigator.onLine</code> property here to demonstrate how the system would work. The <code>navigator.onLine</code> property is <strong>unreliable</strong>, and I would suggest you come up with a custom solution to check whether the user is connected to the internet or not. One way you can do this is by sending a ping request to a server endpoint you’ve created.</p>

<h3 id="registering-the-sync-event">Registering The Sync Event</h3>

<p>Once the network test fails, the next step is to register a sync event. The sync event needs to be registered at the point where the system fails to upload the image due to a poor internet connection.</p>

<pre><code class="language-javascript">async function registerSyncEvent() {
  if ('SyncManager' in window) {
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('uploadImages');
    console.log('Background Sync registered');
  }
}
</code></pre>

<p>After registering the sync event, you need to listen for it in the service worker.</p>

<pre><code class="language-javascript">self.addEventListener('sync', (event) =&gt; {
  if (event.tag === 'uploadImages') {
    event.waitUntil(sendImages());
  }
});
</code></pre>

<p>The <code>sendImages</code> function is going to be an asynchronous process that will retrieve the image from <code>IndexedDB</code> and upload it to the server. This is what it’s going to look like:</p>

<pre><code class="language-javascript">async function sendImages() {
  try {
    // await image retrieval and upload
  } catch (error) {
    // throw error
  }
}
</code></pre>

<h3 id="opening-the-database">Opening The Database</h3>

<p>The first thing we need to do in order to store our image locally is to open an <code>IndexedDB</code> store. As you can see from the code below, we are creating <strong>a global variable to store the database instance</strong>. The reason for doing this is that, subsequently, when we want to retrieve our image from <code>IndexedDB</code>, we wouldn’t need to write the code to open the database again.</p>

<div class="break-out">
<pre><code class="language-javascript">let database; // Global variable to store the database instance

function openDatabase() {
  return new Promise((resolve, reject) =&gt; {
    if (database) return resolve(database); // Return existing database instance 

    const request = indexedDB.open("myDatabase", 1);

    request.onerror = (event) =&gt; {
      console.error("Database error:", event.target.error);
      reject(event.target.error); // Reject the promise on error
    };

    request.onupgradeneeded = (event) =&gt; {
        const db = event.target.result;
        // Create the "images" object store if it doesn't exist.
        if (!db.objectStoreNames.contains("images")) {
          db.createObjectStore("images", { keyPath: "id" });
        }
        console.log("Database setup complete.");
    };

    request.onsuccess = (event) =&gt; {
      database = event.target.result; // Store the database instance globally
      resolve(database); // Resolve the promise with the database instance
    };
  });
}
</code></pre>
</div>

<div class="partners__lead-place"></div>

<h3 id="storing-the-image-in-indexeddb">Storing The Image In IndexedDB</h3>

<p>With the <code>IndexedDB</code> store open, we can now store our images.</p>

<blockquote>Now, you may be wondering why an easier solution like <code>localStorage</code> wasn’t used for this purpose.<br /><br />The reason for that is that <code>IndexedDB</code> operates asynchronously and doesn’t block the main JavaScript thread, whereas <code>localStorage</code> runs synchronously and can block the JavaScript main thread if it is being used.</blockquote>

<p>Here’s how you can store the image in <code>IndexedDB</code>:</p>

<div class="break-out">
<pre><code class="language-javascript">async function storeImages(file) {
  // Open the IndexedDB database.
  const db = await openDatabase();
  // Create a transaction with read and write access.
  const transaction = db.transaction("images", "readwrite");
  // Access the "images" object store.
  const store = transaction.objectStore("images");
  // Define the image record to be stored.
  const imageRecord = {
    id: IMAGE&#95;ID,   // a unique ID
    image: file     // Store the image file (Blob)
  };
  // Add the image record to the store.
  const addRequest = store.add(imageRecord);
  // Handle successful addition.
  addRequest.onsuccess = () =&gt; console.log("Image added successfully!");
  // Handle errors during insertion.
  addRequest.onerror = (e) =&gt; console.error("Error storing image:", e.target.error);
}
</code></pre>
</div>

<p>With the images stored and the background sync set, the system is ready to upload the image whenever the network connection is restored.</p>

<h3 id="retrieving-and-uploading-the-images">Retrieving And Uploading The Images</h3>

<p>Once the network connection is restored, the sync event will fire, and the service worker will retrieve the image from <code>IndexedDB</code> and upload it.</p>

<div class="break-out">
<pre><code class="language-javascript">async function retrieveAndUploadImage(IMAGE&#95;ID) {
  try {
    const db = await openDatabase(); // Ensure the database is open
    const transaction = db.transaction("images", "readonly");
    const store = transaction.objectStore("images");
    const request = store.get(IMAGE&#95;ID);
    request.onsuccess = function (event) {
      const image = event.target.result;
      if (image) {
        // upload Image to server here
      } else {
        console.log("No image found with ID:", IMAGE&#95;ID);
      }
    };
    request.onerror = () =&gt; {
        console.error("Error retrieving image.");
    };
  } catch (error) {
    console.error("Failed to open database:", error);
  }
}
</code></pre>
</div>

<h3 id="deleting-the-indexeddb-database">Deleting The IndexedDB Database</h3>

<p>Once the image has been uploaded, the <code>IndexedDB</code> store is no longer needed. Therefore, it should be deleted along with its content to free up storage.</p>

<div class="break-out">
<pre><code class="language-javascript">function deleteDatabase() {
  // Check if there's an open connection to the database.
  if (database) {
    database.close(); // Close the database connection
    console.log("Database connection closed.");
  }

  // Request to delete the database named "myDatabase".
  const deleteRequest = indexedDB.deleteDatabase("myDatabase");

  // Handle successful deletion of the database.
  deleteRequest.onsuccess = function () {
    console.log("Database deleted successfully!");
  };

  // Handle errors that occur during the deletion process.
  deleteRequest.onerror = function (event) {
    console.error("Error deleting database:", event.target.error);
  };

  // Handle cases where the deletion is blocked (e.g., if there are still open connections).
  deleteRequest.onblocked = function () {
    console.warn("Database deletion blocked. Close open connections and try again.");
  };
}
</code></pre>
</div>
    

<p>With that, the entire process is complete!</p>

<div class="partners__lead-place"></div>

<h2 id="considerations-and-limitations">Considerations And Limitations</h2>

<p>While we’ve done a lot to help improve the experience by supporting offline uploads, the system is not without its limitations. I figured I would specifically call those out because it’s worth knowing where this solution might fall short of your needs.</p>

<ul>
<li><strong>No Reliable Internet Connectivity Detection</strong><br />
JavaScript does not provide a foolproof way to detect online status. For this reason, you need to come up with a custom solution for detecting online status.</li>
<li><strong>Chromium-Only Solution</strong><br />
The Background Sync API is currently <a href="https://developer.mozilla.org/en-US/docs/Web/API/Background_Synchronization_API#browser_compatibility">limited to Chromium-based browsers</a>. As such, this solution is only supported by Chromium browsers. That means you will need a more robust solution if you have the majority of your users on non-Chromium browsers.</li>
<li><strong><code>IndexedDB</code> Storage Policies</strong><br />
Browsers impose storage limitations and eviction policies for <code>IndexedDB</code>. For instance, in Safari, data stored in <code>IndexedDB</code> has a lifespan of seven days if the user doesn’t interact with the website. This is something you should bear in mind if you do come up with an alternative for the background sync API that supports Safari.</li>
</ul>

<h2 id="enhancing-the-user-experience">Enhancing The User Experience</h2>

<p>Since the entire process happens in the background, we need a way to inform the users when images are stored, waiting to be uploaded, or have been successfully uploaded. Implementing certain <strong>UI elements</strong> for this purpose will indeed enhance the experience for the users. These UI elements may include toast notifications, upload status indicators like spinners (to show active processes), progress bars (to show state progress), network status indicators, or buttons to provide retry and cancel options.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>Poor internet connectivity can disrupt the user experience of a web application. However, by leveraging PWA technologies such as <code>IndexedDB</code>, service workers, and the Background Sync API, developers can help improve the reliability of web applications for their users, especially those in areas with unreliable internet connectivity.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Amejimaobari Ollornwi</author><title>Creating An Effective Multistep Form For Better User Experience</title><link>https://www.smashingmagazine.com/2024/12/creating-effective-multistep-form-better-user-experience/</link><pubDate>Tue, 03 Dec 2024 10:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/12/creating-effective-multistep-form-better-user-experience/</guid><description>Forms are already notoriously tough to customize and style &amp;mdash; to the extent that we’re already starting to see new ideas for more flexible control. But what we don’t often discuss is designing good-form experiences beyond validation. That’s what Jima Victor discusses in this article, focusing specifically on creating multi-step forms that involve navigation between sections.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/12/creating-effective-multistep-form-better-user-experience/" />
              <title>Creating An Effective Multistep Form For Better User Experience</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Creating An Effective Multistep Form For Better User Experience</h1>
                  
                    
                    <address>Amejimaobari Ollornwi</address>
                  
                  <time datetime="2024-12-03T10:00:00&#43;00:00" class="op-published">2024-12-03T10:00:00+00:00</time>
                  <time datetime="2024-12-03T10:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>For a multistep form, planning involves structuring questions logically across steps, grouping similar questions, and minimizing the number of steps and the amount of required information for each step. Whatever makes each step focused and manageable is what should be aimed for.</p>

<p>In this tutorial, we will create a multistep form for a job application. Here are the details we are going to be requesting from the applicant at each step:</p>

<ul>
<li><strong>Personal Information</strong><br />
Collects applicant’s name, email, and phone number.</li>
<li><strong>Work Experience</strong><br />
Collects the applicant’s most recent company, job title, and years of experience.</li>
<li><strong>Skills &amp; Qualifications</strong><br />
The applicant lists their skills and selects their highest degree.</li>
<li><strong>Review &amp; Submit</strong><br />
This step is not going to collect any information. Instead, it provides an opportunity for the applicant to go back and review the information entered in the previous steps of the form before submitting it.</li>
</ul>

<p>You can think of structuring these questions as a digital way of getting to know somebody. You can’t meet someone for the first time and ask them about their work experience without first asking for their name.</p>

<p>Based on the steps we have above, this is what the body of our HTML with our form should look like. First, the main <code>&lt;form&gt;</code> element:</p>

<pre><code class="language-html">&lt;form id="jobApplicationForm"&gt;
  &lt;!-- Step 1: Personal Information --&gt;
  &lt;!-- Step 2: Work Experience --&gt;
  &lt;!-- Step 3: Skills & Qualifications --&gt;
  &lt;!-- Step 4: Review & Submit --&gt;
&lt;/form&gt;
</code></pre>

<p><strong>Step 1</strong> is for filling in personal information, like the applicant’s name, email address, and phone number:</p>

<div class="break-out">
<pre><code class="language-javascript">&lt;form id="jobApplicationForm"&gt;
  &lt;!-- Step 1: Personal Information --&gt;
  &lt;fieldset class="step" id="step-1"&gt;
    &lt;legend id="step1Label"&gt;Step 1: Personal Information&lt;/legend&gt;
    &lt;label for="name"&gt;Full Name&lt;/label&gt;
    &lt;input type="text" id="name" name="name" required /&gt;
    &lt;label for="email"&gt;Email Address&lt;/label&gt;
    &lt;input type="email" id="email" name="email" required /&gt;
    &lt;label for="phone"&gt;Phone Number&lt;/label&gt;
    &lt;input type="tel" id="phone" name="phone" required /&gt;
  &lt;/fieldset&gt;

  &lt;!-- Step 2: Work Experience --&gt;
  &lt;!-- Step 3: Skills & Qualifications --&gt;
  &lt;!-- Step 4: Review & Submit --&gt;
&lt;/form&gt;
</code></pre>
</div>

<p>Once the applicant completes the first step, we’ll navigate them to <strong>Step 2</strong>, focusing on their work experience so that we can collect information like their most recent company, job title, and years of experience. We’ll tack on a new <code>&lt;fieldset&gt;</code> with those inputs:</p>

<div class="break-out">
<pre><code class="language-javascript">&lt;form id="jobApplicationForm"&gt;
  &lt;!-- Step 1: Personal Information --&gt;

  &lt;!-- Step 2: Work Experience --&gt;
  &lt;fieldset class="step" id="step-2" hidden&gt;
    &lt;legend id="step2Label"&gt;Step 2: Work Experience&lt;/legend&gt;
    &lt;label for="company"&gt;Most Recent Company&lt;/label&gt;
    &lt;input type="text" id="company" name="company" required /&gt;
    &lt;label for="jobTitle"&gt;Job Title&lt;/label&gt;
    &lt;input type="text" id="jobTitle" name="jobTitle" required /&gt;
    &lt;label for="yearsExperience"&gt;Years of Experience&lt;/label&gt;
    &lt;input
      type="number"
      id="yearsExperience"
      name="yearsExperience"
      min="0"
      required
    /&gt;
  &lt;/fieldset&gt;

  &lt;!-- Step 3: Skills & Qualifications --&gt;
  &lt;!-- Step 4: Review & Submit --&gt;
&lt;/form&gt;
</code></pre>
</div>

<p><strong>Step 3</strong> is all about the applicant listing their skills and qualifications for the job they’re applying for:</p>

<div class="break-out">
<pre><code class="language-javascript">&lt;form id="jobApplicationForm"&gt;
  &lt;!-- Step 1: Personal Information --&gt;
  &lt;!-- Step 2: Work Experience --&gt;

  &lt;!-- Step 3: Skills & Qualifications --&gt;
  &lt;fieldset class="step" id="step-3" hidden&gt;
    &lt;legend id="step3Label"&gt;Step 3: Skills & Qualifications&lt;/legend&gt;
    &lt;label for="skills"&gt;Skill(s)&lt;/label&gt;
    &lt;textarea id="skills" name="skills" rows="4" required&gt;&lt;/textarea&gt;
    &lt;label for="highestDegree"&gt;Degree Obtained (Highest)&lt;/label&gt;
    &lt;select id="highestDegree" name="highestDegree" required&gt;
      &lt;option value=""&gt;Select Degree&lt;/option&gt;
      &lt;option value="highschool"&gt;High School Diploma&lt;/option&gt;
      &lt;option value="bachelor"&gt;Bachelor's Degree&lt;/option&gt;
      &lt;option value="master"&gt;Master's Degree&lt;/option&gt;
      &lt;option value="phd"&gt;Ph.D.&lt;/option&gt;
    &lt;/select&gt;
  &lt;/fieldset&gt;
  &lt;!-- Step 4: Review & Submit --&gt;
  &lt;fieldset class="step" id="step-4" hidden&gt;
    &lt;legend id="step4Label"&gt;Step 4: Review & Submit&lt;/legend&gt;
    &lt;p&gt;Review your information before submitting the application.&lt;/p&gt;
    &lt;button type="submit"&gt;Submit Application&lt;/button&gt;
  &lt;/fieldset&gt;
&lt;/form&gt;
</code></pre>
</div>

<p>And, finally, we’ll allow the applicant to review their information before submitting it:</p>

<div class="break-out">
<pre><code class="language-javascript">&lt;form id="jobApplicationForm"&gt;
  &lt;!-- Step 1: Personal Information --&gt;
  &lt;!-- Step 2: Work Experience --&gt;
  &lt;!-- Step 3: Skills & Qualifications --&gt;

  &lt;!-- Step 4: Review & Submit --&gt;
  &lt;fieldset class="step" id="step-4" hidden&gt;
    &lt;legend id="step4Label"&gt;Step 4: Review & Submit&lt;/legend&gt;
    &lt;p&gt;Review your information before submitting the application.&lt;/p&gt;
    &lt;button type="submit"&gt;Submit Application&lt;/button&gt;
  &lt;/fieldset&gt;
&lt;/form&gt;
</code></pre>
</div>

<p><strong>Notice</strong>: We’ve added a <code>hidden</code> attribute to every <code>fieldset</code> element but the first one. This ensures that the user sees only the first step. Once they are done with the first step, they can proceed to fill out their work experience on the second step by clicking a navigational button. We’ll add this button later on.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Meet <strong><a data-instant href="https://www.smashingconf.com/online-workshops/">Smashing Workshops</a></strong> on <strong>front-end, design &amp; UX</strong>, with practical takeaways, live sessions, <strong>video recordings</strong> and a friendly Q&amp;A. With Brad Frost, Stéph Walter and <a href="https://smashingconf.com/online-workshops/workshops">so many others</a>.</p>
<a data-instant href="smashing-workshops" class="btn btn--green btn--large" style="">Jump to the workshops&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="smashing-workshops" class="feature-panel-image-link">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-scubadiving-panel.svg"
    alt="Feature Panel"
    width="257"
    height="355"
/>

</div>
</a>
</div>
</aside>
</div>

<h2 id="adding-styles">Adding Styles</h2>

<p>To keep things focused, we’re not going to be emphasizing the styles in this tutorial. What we’ll do to keep things simple is leverage the <a href="https://simplecss.org">Simple.css style framework</a> to get the form in good shape for the rest of the tutorial.</p>

<p>If you’re following along, we can include Simple’s styles in the document <code>&lt;head&gt;</code>:</p>

<div class="break-out">
<pre><code class="language-javascript">&lt;link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css" /&gt;
</code></pre>
</div>

<p>And from there, go ahead and create a <code>style.css</code> file with the following styles that I’ve folded up.</p>

<pre><code class="language-css">&lt;details&gt;
  &lt;summary&gt;View CSS&lt;/summary&gt;
  body {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  main {
    padding: 0 30px;
  }
  h1 {
    font-size: 1.8rem;
    text-align: center;
  }
  .stepper {
    display: flex;
    justify-content: flex-end;
    padding-right: 10px;
  }
  form {
    box-shadow: 0px 0px 6px 2px rgba(0, 0, 0, 0.2);
    padding: 12px;
  }
  input,
  textarea,
  select {
    outline: none;
  }
  input:valid,
  textarea:valid,
  select:valid,
  input:focus:valid,
  textarea:focus:valid,
  select:focus:valid {
    border-color: green;
  }
  input:focus:invalid,
  textarea:focus:invalid,
  select:focus:invalid {
    border: 1px solid red;
  }
&lt;/details&gt;
</code></pre>

<h2 id="form-navigation-and-validation">Form Navigation And Validation</h2>

<p>An easy way to ruin the user experience for a multi-step form is to wait until the user gets to the last step in the form before letting them know of any error they made along the way. Each step of the form should be validated for errors before moving on to the next step, and descriptive error messages should be displayed to enable users to understand what is wrong and how to fix it.</p>

<p>Now, the only part of our form that is visible is the first step. To complete the form, users need to be able to navigate to the other steps. We are going to use several buttons to pull this off. The first step is going to have a <kbd>Next</kbd> button. The second and third steps are going to have both a <kbd>Previous</kbd> and a <kbd>Next</kbd> button, and the fourth step is going to have a <kbd>Previous</kbd> and a <kbd>Submit</kbd> button.</p>

<div class="break-out">
<pre><code class="language-html">&lt;form id="jobApplicationForm"&gt;
  &lt;!-- Step 1: Personal Information --&gt;
  &lt;fieldset&gt;
    &lt;!-- ... --&gt;
    &lt;button type="button" class="next" onclick="nextStep()"&gt;Next&lt;/button&gt;
  &lt;/fieldset&gt;

  &lt;!-- Step 2: Work Experience --&gt;
  &lt;fieldset&gt;
    &lt;!-- ... --&gt;
    &lt;button type="button" class="previous" onclick="previousStep()"&gt;Previous&lt;/button&gt;
    &lt;button type="button" class="next" onclick="nextStep()"&gt;Next&lt;/button&gt;
  &lt;/fieldset&gt;

  &lt;!-- Step 3: Skills & Qualifications --&gt;
  &lt;fieldset&gt;
    &lt;!-- ... --&gt;
    &lt;button type="button" class="previous" onclick="previousStep()"&gt;Previous&lt;/button&gt;
    &lt;button type="button" class="next" onclick="nextStep()"&gt;Next&lt;/button&gt;
  &lt;/fieldset&gt;

  &lt;!-- Step 4: Review & Submit --&gt;
  &lt;fieldset&gt;
    &lt;!-- ... --&gt;
    &lt;button type="button" class="previous" onclick="previousStep()"&gt;Previous&lt;/button&gt;
    &lt;button type="submit"&gt;Submit Application&lt;/button&gt;
  &lt;/fieldset&gt;
&lt;/form&gt;
</code></pre>
</div>

<p><strong>Notice</strong>: We’ve added <code>onclick</code> attributes to the <kbd>Previous</kbd> and <kbd>Next</kbd> buttons to link them to their respective JavaScript functions: <code>previousStep()</code> and <code>nextStep()</code>.</p>

<div class="partners__lead-place"></div>

<h3 id="the-next-button">The “Next” Button</h3>

<p>The <code>nextStep()</code> function is linked to the Next button. Whenever the user clicks the Next button, the <code>nextStep()</code> function will first check to ensure that all the fields for whatever step the user is on have been filled out correctly before moving on to the next step. If the fields haven’t been filled correctly, it displays some error messages, letting the user know that they’ve done something wrong and informing them what to do to make the errors go away.</p>

<p>Before we go into the implementation of the <code>nextStep</code> function, there are certain variables we need to define because they will be needed in the function. First, we need the input fields from the DOM so we can run checks on them to make sure they are valid.</p>

<div class="break-out">
<pre><code class="language-javascript">// Step 1 fields
const name = document.getElementById("name");
const email = document.getElementById("email");
const phone = document.getElementById("phone");

// Step 2 fields
const company = document.getElementById("company");
const jobTitle = document.getElementById("jobTitle");
const yearsExperience = document.getElementById("yearsExperience");

// Step 3 fields
const skills = document.getElementById("skills");
const highestDegree = document.getElementById("highestDegree");
</code></pre>
</div>

<p>Then, we’re going to need an array to store our error messages.</p>

<pre><code class="language-javascript">let errorMsgs = [];
</code></pre>

<p>Also, we would need an element in the DOM where we can insert those error messages after they’ve been generated. This element should be placed in the HTML just below the last <code>fieldset</code> closing tag:</p>

<pre><code class="language-html">&lt;div id="errorMessages" style="color: rgb(253, 67, 67)"&gt;&lt;/div&gt;
</code></pre>

<p>Add the above <code>div</code> to the JavaScript code using the following line:</p>

<pre><code class="language-javascript">const errorMessagesDiv = document.getElementById("errorMessages");
</code></pre>

<p>And finally, we need a variable to keep track of the current step.</p>

<p><code class="language-javascript">let currentStep = 1;
</code></pre></p>

<p>Now that we have all our variables in place, here’s the implementation of the <code>nextstep()</code> function:</p>

<div class="break-out">
<pre><code class="language-javascript">function nextStep() {
  errorMsgs = [];
  errorMessagesDiv.innerText = "";

  switch (currentStep) {
    case 1:
      addValidationErrors(name, email, phone);
      validateStep(errorMsgs);
      break;

    case 2:
      addValidationErrors(company, jobTitle, yearsExperience);
      validateStep(errorMsgs);
      break;

    case 3:
      addValidationErrors(skills, highestDegree);
      validateStep(errorMsgs);
      break;
  }
}
</code></pre>
</div>

<p>The moment the <kbd>Next</kbd> button is pressed, our code first checks which step the user is currently on, and based on this information, it validates the data for that specific step by calling the <code>addValidationErrors()</code> function. If there are errors, we display them. Then, the form calls the <code>validateStep()</code> function to verify that there are no errors before moving on to the next step. If there are errors, it prevents the user from going on to the next step.</p>

<p>Whenever the <code>nextStep()</code> function runs, the error messages are cleared first to avoid appending errors from a different step to existing errors or re-adding existing error messages when the <code>addValidationErrors</code> function runs. The <code>addValidationErrors</code> function is called for each step using the fields for that step as arguments.</p>

<p>Here’s how the <code>addValidationErrors</code> function is implemented:</p>

<div class="break-out">
<pre><code class="language-javascript">function addValidationErrors(fieldOne, fieldTwo, fieldThree = undefined) {
  if (!fieldOne.checkValidity()) {
    const label = document.querySelector(`label[for="${fieldOne.id}"]`);
    errorMsgs.push(`Please Enter A Valid ${label.textContent}`);
  }

  if (!fieldTwo.checkValidity()) {
    const label = document.querySelector(`label[for="${fieldTwo.id}"]`);
    errorMsgs.push(`Please Enter A Valid ${label.textContent}`);
  }

  if (fieldThree && !fieldThree.checkValidity()) {
    const label = document.querySelector(`label[for="${fieldThree.id}"]`);
    errorMsgs.push(`Please Enter A Valid ${label.textContent}`);
  }

  if (errorMsgs.length &gt; 0) {
    errorMessagesDiv.innerText = errorMsgs.join("\n");
  }
}
</code></pre>
</div>

<p>This is how the <code>validateStep()</code> function is defined:</p>

<pre><code class="language-javascript">function validateStep(errorMsgs) {
  if (errorMsgs.length === 0) {
    showStep(currentStep + 1);
  }
}
</code></pre>

<p>The <code>validateStep()</code> function checks for errors. If there are none, it proceeds to the next step with the help of the <code>showStep()</code> function.</p>

<pre><code class="language-javascript">function showStep(step) {
  steps.forEach((el, index) =&gt; {
    el.hidden = index + 1 !== step;
  });
  currentStep = step;
}
</code></pre>

<p>The <code>showStep()</code> function requires the four fieldsets in the DOM. Add the following line to the top of the JavaScript code to make the fieldsets available:</p>

<pre><code class="language-javascript">const steps = document.querySelectorAll(".step");
</code></pre>

<p>What the <code>showStep()</code> function does is to go through all the <code>fieldsets</code> in our form and hide whatever <code>fieldset</code> is not equal to the one we’re navigating to. Then, it updates the <code>currentStep</code> variable to be equal to the step we’re navigating to.</p>

<h3 id="the-previous-button">The “Previous” Button</h3>

<p>The <code>previousStep()</code> function is linked to the <kbd>Previous</kbd> button. Whenever the previous button is clicked, similarly to the <code>nextStep</code> function, the error messages are also cleared from the page, and navigation is also handled by the <code>showStep</code> function.</p>

<pre><code class="language-javascript">function previousStep() {
  errorMessagesDiv.innerText = "";
  showStep(currentStep - 1);
}
</code></pre>

<p>Whenever the <code>showStep()</code> function is called with “<code>currentStep - 1</code>” as an argument (as in this case), we go back to the previous step, while moving to the next step happens by calling the <code>showStep()</code> function with “<code>currentStep + 1</code>&rdquo; as an argument (as in the case of the <code>validateStep()</code> function).</p>

<div class="partners__lead-place"></div>

<h2 id="improving-user-experience-with-visual-cues">Improving User Experience With Visual Cues</h2>

<p>One other way of improving the user experience for a multi-step form, is by integrating visual cues, things that will give users feedback on the process they are on. These things can include a progress indicator or a stepper to help the user know the exact step they are on.</p>

<h3 id="integrating-a-stepper">Integrating A Stepper</h3>

<p>To integrate a stepper into our form (sort of like <a href="https://m1.material.io/components/steppers.html#">this one</a> from Material Design), the first thing we need to do is add it to the HTML just below the opening <code>&lt;form&gt;</code> tag.</p>

<pre><code class="language-html">&lt;form id="jobApplicationForm"&gt;
  &lt;div class="stepper"&gt;
    &lt;span&gt;&lt;span class="currentStep"&gt;1&lt;/span&gt;/4&lt;/span&gt;
  &lt;/div&gt;
  &lt;!-- ... --&gt;
&lt;/form&gt;
</code></pre>

<p>Next, we need to query the part of the stepper that will represent the current step. This is the span tag with the class name of <code>currentStep</code>.</p>

<pre><code class="language-javascript">const currentStepDiv = document.querySelector(".currentStep");
</code></pre>

<p>Now, we need to update the stepper value whenever the previous or next buttons are clicked. To do this, we need to update the <code>showStep()</code> function by appending the following line to it:</p>

<pre><code class="language-javascript">currentStepDiv.innerText = currentStep;
</code></pre>

<p>This line is added to the <code>showStep()</code> function because the <code>showStep()</code> function is responsible for navigating between steps and updating the <code>currentStep</code> variable. So, whenever the <code>currentStep</code> variable is updated, the currentStepDiv should also be updated to reflect that change.</p>

<h3 id="storing-and-retrieving-user-data">Storing And Retrieving User Data</h3>

<p>One major way we can improve the form’s user experience is by storing user data in the browser. Multistep forms are usually long and require users to enter a lot of information about themselves. Imagine a user filling out 95% of a form, then accidentally hitting the <kbd>F5</kbd> button on their keyboard and losing all their progress. That would be a really bad experience for the user.</p>

<p>Using <code>localStorage</code>, we can store user information as soon as it is entered and retrieve it as soon as the DOM content is loaded, so users can always continue filling out their forms from wherever they left off. To add this feature to our forms, we can begin by saving the user’s information as soon as it is typed. This can be achieved using the <code>input</code> event.</p>

<p>Before adding the <code>input</code> event listener, get the form element from the DOM:</p>

<pre><code class="language-javascript">const form = document.getElementById("jobApplicationForm");
</code></pre>

<p>Now we can add the <code>input</code> event listener:</p>

<div class="break-out">
<pre><code class="language-javascript">// Save data on each input event
form.addEventListener("input", () =&gt; {
  const formData = {
    name: document.getElementById("name").value,
    email: document.getElementById("email").value,
    phone: document.getElementById("phone").value,
    company: document.getElementById("company").value,
    jobTitle: document.getElementById("jobTitle").value,
    yearsExperience: document.getElementById("yearsExperience").value,
    skills: document.getElementById("skills").value,
    highestDegree: document.getElementById("highestDegree").value,
  };
  localStorage.setItem("formData", JSON.stringify(formData));
});
</code></pre>
</div>

<p>Next, we need to add some code to help us retrieve the user data once the DOM content is loaded.</p>

<div class="break-out">
<pre><code class="language-javascript">window.addEventListener("DOMContentLoaded", () =&gt; {
  const savedData = JSON.parse(localStorage.getItem("formData"));
  if (savedData) {
    document.getElementById("name").value = savedData.name || "";
    document.getElementById("email").value = savedData.email || "";
    document.getElementById("phone").value = savedData.phone || "";
    document.getElementById("company").value = savedData.company || "";
    document.getElementById("jobTitle").value = savedData.jobTitle || "";
    document.getElementById("yearsExperience").value = savedData.yearsExperience || "";
    document.getElementById("skills").value = savedData.skills || "";
    document.getElementById("highestDegree").value = savedData.highestDegree || "";
  }
});
</code></pre>
</div>

<p>Lastly, it is good practice to remove data from <code>localStorage</code> as soon as it is no longer needed:</p>

<pre><code class="language-javascript">// Clear data on form submit
form.addEventListener('submit', () =&gt; {
  // Clear localStorage once the form is submitted
  localStorage.removeItem('formData');
}); 
</code></pre>

<h3 id="adding-the-current-step-value-to-localstorage">Adding The Current Step Value To <code>localStorage</code></h3>

<p>If the user accidentally closes their browser, they should be able to return to wherever they left off. This means that the current step value also has to be saved in <code>localStorage</code>.</p>

<p>To save this value, append the following line to the <code>showStep()</code> function:</p>

<pre><code class="language-javascript">localStorage.setItem("storedStep", currentStep);
</code></pre>

<p>Now we can retrieve the current step value and return users to wherever they left off whenever the DOM content loads. Add the following code to the <code>DOMContentLoaded</code> handler to do so:</p>

<pre><code class="language-javascript">const storedStep = localStorage.getItem("storedStep");

if (storedStep) {
    const storedStepInt = parseInt(storedStep);
    steps.forEach((el, index) =&gt; {
      el.hidden = index + 1 !== storedStepInt;
    });
    currentStep = storedStepInt;
    currentStepDiv.innerText = currentStep;
  }
</code></pre>

<p>Also, do not forget to clear the current step value from <code>localStorage</code> when the form is submitted.</p>

<pre><code class="language-javascript">localStorage.removeItem("storedStep");
</code></pre>

<p>The above line should be added to the submit handler.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>Creating multi-step forms can help improve user experience for complex data entry. By carefully planning out steps, implementing form validation at each step, and temporarily storing user data in the browser, you make it easier for users to complete long forms.</p>

<p>For the full implementation of this multi-step form, you can access the complete code on <a href="https://github.com/jimavictor/multistep-form">GitHub</a>.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Karin Hendrikse</author><title>Build A Static RSS Reader To Fight Your Inner FOMO</title><link>https://www.smashingmagazine.com/2024/10/build-static-rss-reader-fight-fomo/</link><pubDate>Mon, 07 Oct 2024 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/10/build-static-rss-reader-fight-fomo/</guid><description>RSS is a classic technology that fetches content from websites and feeds it to anyone who subscribes to it with a URL. It’s based on XML, and we can use it to consume the feeds in our own apps. Karin Hendrikse demonstrates how to do exactly that with a static site you can use as your personal RSS reader.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/10/build-static-rss-reader-fight-fomo/" />
              <title>Build A Static RSS Reader To Fight Your Inner FOMO</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Build A Static RSS Reader To Fight Your Inner FOMO</h1>
                  
                    
                    <address>Karin Hendrikse</address>
                  
                  <time datetime="2024-10-07T13:00:00&#43;00:00" class="op-published">2024-10-07T13:00:00+00:00</time>
                  <time datetime="2024-10-07T13:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>In a fast-paced industry like tech, it can be hard to deal with the fear of missing out on important news. But, as many of us know, there’s an absolutely huge amount of information coming in daily, and finding the right time and balance to keep up can be difficult, if not stressful. A classic piece of technology like <strong>an RSS feed is a delightful way of taking back ownership of our own time</strong>. In this article, we will create a static Really Simple Syndication (RSS) reader that will bring you the latest curated news only once (yes: <em>once</em>) a day.</p>

<p>We’ll obviously work with RSS technology in the process, but we’re also going to combine it with some things that maybe you haven’t tried before, including <strong>Astro</strong> (the static site framework), <strong>TypeScript</strong> (for JavaScript goodies), a package called <strong>rss-parser</strong> (for connecting things together), as well as <strong>scheduled functions</strong> and <strong>build hooks</strong> provided by Netlify (although there are other services that do this).</p>

<p>I chose these technologies purely because I really, really enjoy them! There may be other solutions out there that are more performant, come with more features, or are simply more comfortable to you &mdash; and in those cases, I encourage you to swap in whatever you’d like. The most important thing is getting the end result!</p>

<h2 id="the-plan">The Plan</h2>

<p>Here’s how this will go. Astro generates the website. I made the intentional decision to use a static site because I want the different RSS feeds to be fetched only once during build time, and that’s something we can control each time the site is “rebuilt” and redeployed with updates. That’s where Netlify’s scheduled functions come into play, as they let us trigger rebuilds automatically at specific times. There is no need to manually check for updates and deploy them! Cron jobs can just as readily do this if you prefer a server-side solution.</p>

<p>During the triggered rebuild, we’ll let the rss-parser package do exactly what it says it does: parse a list of RSS feeds that are contained in an array. The package also allows us to set a filter for the fetched results so that we only get ones from the past day, week, and so on. Personally, I only render the news from the last seven days to prevent content overload. We’ll get there!</p>

<p>But first&hellip;</p>

<h2 id="what-is-rss">What Is RSS?</h2>

<p>RSS is a web feed technology that you can feed into a reader or news aggregator. Because RSS is standardized, you know what to expect when it comes to the feed’s format. That means we have a ton of fun possibilities when it comes to handling the data that the feed provides. Most news websites have their own RSS feed that you can subscribe to (this is <strong>Smashing Magazine’s RSS feed</strong>: <a href="https://www.smashingmagazine.com/feed/">https://www.smashingmagazine.com/feed/</a>). An RSS feed is capable of updating every time a site publishes new content, which means it can be a quick source of the latest news, but we can tailor that frequency as well.</p>

<p>RSS feeds are written in an Extensible Markup Language (XML) format and have specific elements that can be used within it. Instead of focusing too much on the technicalities here, I’ll give you a link to the <a href="https://www.rssboard.org/rss-specification">RSS specification</a>. Don’t worry; that page should be scannable enough for you to find the most pertinent information you need, like the kinds of elements that are supported and what they represent. For this tutorial, we’re only using the following elements: <strong><code>&lt;title&gt;</code></strong>, <strong><code>&lt;link&gt;</code></strong>, <strong><code>&lt;description&gt;</code></strong>, <strong><code>&lt;item&gt;</code></strong>, and <strong><code>&lt;pubDate&gt;</code></strong>. We’ll also let our RSS parser package do some of the work for us.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p><strong>Web forms</strong> are at the center of every meaningful interaction. Meet Adam Silver&rsquo;s <strong><a href="https://www.smashingmagazine.com/printed-books/form-design-patterns/">Form Design Patterns</a></strong>, a practical guide to <strong>designing and building forms</strong> for the web.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/form-design-patterns/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://www.smashingmagazine.com/printed-books/form-design-patterns/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/64e57b41-b7f1-4ae3-886a-806cce580ef9/form-design-patterns-shop-image-1-1.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/51e0f837-d85d-4b28-bfab-1c9a47f0ce33/form-design-patterns-shop-image.png"
    alt="Feature Panel"
    width="481"
    height="698"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="creating-the-state-site">Creating The State Site</h2>

<p>We’ll start by creating our Astro site! In your terminal run <code>pnpm create astro@latest</code>. You can use any package manager you want &mdash; I’m simply trying out <a href="https://pnpm.io">pnpm</a> for myself.</p>

<p>After running the command, Astro’s chat-based helper, Houston, walks through some setup questions to get things started.</p>

<pre><code class="language-bash"> astro   Launch sequence initiated.

   dir   Where should we create your new project?
         ./rss-buddy

  tmpl   How would you like to start your new project?
         Include sample files

    ts   Do you plan to write TypeScript?
         Yes

   use   How strict should TypeScript be?
         Strict

  deps   Install dependencies?
         Yes

   git   Initialize a new git repository?
         Yes
</code></pre>

<p>I like to use Astro’s sample files so I can get started quickly, but we’re going to clean them up a bit in the process. Let’s clean up the <code>src/pages/index.astro</code> file by removing everything inside of the <code>&lt;main&gt;&lt;/main&gt;</code> tags. Then we’re good to go!</p>

<p>From there, we can spin things by running <code>pnpm start</code>. Your terminal will tell you which localhost address you can find your site at.</p>

<div class="partners__lead-place"></div>

<h2 id="pulling-information-from-rss-feeds">Pulling Information From RSS feeds</h2>

<p>The <code>src/pages/index.astro</code> file is where we will make an array of RSS feeds we want to follow. We will be using <a href="https://docs.astro.build/en/basics/astro-syntax/">Astro’s template syntax</a>, so between the two code fences (&mdash;), create an array of <code>feedSources</code> and add some feeds. If you need inspiration, you can copy this:</p>

<pre><code class="language-javascript">const feedSources = [
  'https://www.smashingmagazine.com/feed/',
  'https://developer.mozilla.org/en-US/blog/rss.xml',
  // etc.
]
</code></pre>

<p>Now we’ll install the <a href="https://github.com/rbren/rss-parser">rss-parser package</a> in our project by running <code>pnpm install rss-parser</code>. This package is a small library that turns the XML that we get from fetching an RSS feed into JavaScript objects. This makes it easy for us to read our RSS feeds and manipulate the data any way we want.</p>

<p>Once the package is installed, open the <code>src/pages/index.astro</code> file, and at the top, we’ll import the rss-parser and instantiate the <code>Partner</code> class.</p>

<pre><code class="language-javascript">import Parser from 'rss-parser';
const parser = new Parser();
</code></pre>

<p>We use this parser to read our RSS feeds and (surprise!) <em>parse</em> them to JavaScript. We’re going to be dealing with a list of promises here. Normally, I would probably use <code>Promise.all()</code>, but the thing is, this is supposed to be a complicated experience. If one of the feeds doesn’t work for some reason, I’d prefer to simply ignore it.</p>

<p>Why? Well, because <code>Promise.all()</code> rejects everything even if only one of its promises is rejected. That might mean that if one feed doesn’t behave the way I’d expect it to, my entire page would be blank when I grab my hot beverage to read the news in the morning. I do not want to start my day confronted by an error.</p>

<p>Instead, I’ll opt to use <code>Promise.allSettled()</code>. This method will actually let all promises complete even if one of them fails. In our case, this means any feed that errors will just be ignored, which is perfect.</p>

<p>Let’s add this to the <code>src/pages/index.astro</code> file:</p>

<div class="break-out">
<pre><code class="language-typescript">interface FeedItem {
  feed?: string;
  title?: string;
  link?: string;
  date?: Date;
}

const feedItems: FeedItem[] = [];

await Promise.allSettled(
  feedSources.map(async (source) =&gt; {
    try {
      const feed = await parser.parseURL(source);
      feed.items.forEach((item) =&gt; {
        const date = item.pubDate ? new Date(item.pubDate) : undefined;
        
          feedItems.push({
            feed: feed.title,
            title: item.title,
            link: item.link,
            date,
          });
      });
    } catch (error) {
      console.error(`Error fetching feed from ${source}:`, error);
    }
  })
);
</code></pre>
</div>

<p>This creates an array (or more) named <code>feedItems</code>. For each URL in the <code>feedSources</code> array we created earlier, the rss-parser retrieves the items and, yes, parses them into JavaScript. Then, we return whatever data we want! We’ll keep it simple for now and only return the following:</p>

<ul>
<li>The feed title,</li>
<li>The title of the feed item,</li>
<li>The link to the item,</li>
<li>And the item’s published date.</li>
</ul>

<p>The next step is to ensure that all items are sorted by date so we’ll truly get the “latest” news. Add this small piece of code to our work:</p>

<div class="break-out">
<pre><code class="language-typescript">const sortedFeedItems = feedItems.sort((a, b) =&gt; (b.date ?? new Date()).getTime() - (a.date ?? new Date()).getTime());
</code></pre>
</div>

<p>Oh, and&hellip; remember when I said I didn’t want this RSS reader to render anything older than seven days? Let’s tackle that right now since we’re already in this code.</p>

<p>We’ll make a new variable called <code>sevenDaysAgo</code> and assign it a date. We’ll then set that date to seven days ago and use that logic before we add a new item to our <code>feedItems</code> array.</p>

<p>This is what the <code>src/pages/index.astro</code> file should now look like at this point:</p>

<div class="break-out">
<pre><code class="language-typescript">---
import Layout from '../layouts/Layout.astro';
import Parser from 'rss-parser';
const parser = new Parser();

const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

const feedSources = [
  'https://www.smashingmagazine.com/feed/',
  'https://developer.mozilla.org/en-US/blog/rss.xml',
]

interface FeedItem {
  feed?: string;
  title?: string;
  link?: string;
  date?: Date;
}

const feedItems: FeedItem[] = [];

await Promise.allSettled(
  feedSources.map(async (source) =&gt; {
    try {
      const feed = await parser.parseURL(source);
      feed.items.forEach((item) =&gt; {
        const date = item.pubDate ? new Date(item.pubDate) : undefined;
        if (date && date &gt;= sevenDaysAgo) {
          feedItems.push({
            feed: feed.title,
            title: item.title,
            link: item.link,
            date,
          });
        }
      });
    } catch (error) {
      console.error(`Error fetching feed from ${source}:`, error);
    }
  })
);

const sortedFeedItems = feedItems.sort((a, b) =&gt; (b.date ?? new Date()).getTime() - (a.date ?? new Date()).getTime());

---

&lt;Layout title="Welcome to Astro."&gt;
  &lt;main&gt;
  &lt;/main&gt;
&lt;/Layout&gt;
</code></pre>
</div>

<div class="partners__lead-place"></div>

<h2 id="rendering-xml-data">Rendering XML Data</h2>

<p>It’s time to show our news articles on the Astro site! To keep this simple, we’ll format the items in an unordered list rather than some other fancy layout.</p>

<p>All we need to do is update the <code>&lt;Layout&gt;</code> element in the file with the XML objects sprinkled in for a feed item’s title, URL, and publish date.</p>

<pre><code class="language-html">&lt;Layout title="Welcome to Astro."&gt;
  &lt;main&gt;
  {sortedFeedItems.map(item =&gt; (
    &lt;ul&gt;
      &lt;li&gt;
        &lt;a href={item.link}&gt;{item.title}&lt;/a&gt;
        &lt;p&gt;{item.feed}&lt;/p&gt;
        &lt;p&gt;{item.date}&lt;/p&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  ))}
  &lt;/main&gt;
&lt;/Layout&gt;
</code></pre>

<p>Go ahead and run <code>pnpm start</code> from the terminal. The page should display an unordered list of feed items. Of course, everything is styled at the moment, but luckily for you, you can make it look exactly like you want with CSS!</p>

<p>And remember that there are even <strong>more fields available in the XML for each item</strong> if you want to display more information. If you run the following snippet in your DevTools console, you’ll see all of the fields you have at your disposal:</p>

<pre><code class="language-javascript">feed.items.forEach(item =&gt; {}
</code></pre>

<h2 id="scheduling-daily-static-site-builds">Scheduling Daily Static Site Builds</h2>

<p>We’re nearly done! The feeds are being fetched, and they are returning data back to us in JavaScript for use in our Astro page template. Since feeds are updated whenever new content is published, we need a way to fetch the latest items from it.</p>

<p>We want to avoid doing any of this manually. So, let’s set this site on Netlify to gain access to their scheduled functions that trigger a rebuild and their build hooks that do the building. Again, other services do this, and you’re welcome to roll this work with another provider &mdash; I’m just partial to Netlify since I work there. In any case, you can follow Netlify’s documentation for <a href="https://docs.netlify.com/welcome/add-new-site/#import-from-an-existing-repository">setting up a new site</a>.</p>

<p>Once your site is hosted and live, you are ready to schedule your rebuilds. A <a href="https://docs.netlify.com/configure-builds/build-hooks/">build hook</a> gives you a URL to use to trigger the new build, looking something like this:</p>

<pre><code class="language-html">https://api.netlify.com/build_hooks/your-build-hook-id
</code></pre>

<p>Let’s trigger builds every day at midnight. We’ll use Netlify’s <a href="https://docs.netlify.com/functions/scheduled-functions/">scheduled functions</a>. That’s really why I’m using Netlify to host this in the first place. Having them at the ready via the host greatly simplifies things since there’s no server work or complicated configurations to get this going. Set it and forget it!</p>

<p>We’ll install <code>@netlify/functions</code> (<a href="https://docs.netlify.com/functions/get-started/">instructions</a>) to the project and then create the following file in the project’s root directory: <code>netlify/functions/deploy.ts</code>.</p>

<p>This is what we want to add to that file:</p>

<div class="break-out">
<pre><code class="language-typescript">// netlify/functions/deploy.ts

import type { Config } from '@netlify/functions';

const BUILD_HOOK =
  'https://api.netlify.com/build_hooks/your-build-hook-id'; // replace me!

export default async (req: Request) =&gt; {
  await fetch(BUILD&#95;HOOK, {
    method: 'POST',
  })
};

export const config: Config = {
  schedule: '0 0 &#42; &#42; &#42;',
};
</code></pre>
</div>

<p>If you commit your code and push it, your site should re-deploy automatically. From that point on, it follows a schedule that rebuilds the site every day at midnight, ready for you to take your morning brew and catch up on everything that <em>you</em> think is important.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Amejimaobari Ollornwi</author><title>Generating Unique Random Numbers In JavaScript Using Sets</title><link>https://www.smashingmagazine.com/2024/08/generating-unique-random-numbers-javascript-using-sets/</link><pubDate>Mon, 26 Aug 2024 15:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/08/generating-unique-random-numbers-javascript-using-sets/</guid><description>Want to create more randomized effects in your JavaScript code? The &lt;code>Math.random()&lt;/code> method alone, with its limitations, won’t cut it for generating unique random numbers. Amejimaobari Ollornwi explains how to generate a series of unique random numbers using the &lt;code>Set&lt;/code> object, how to use these random numbers as indexes for arrays, and explores some practical applications of randomization.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/08/generating-unique-random-numbers-javascript-using-sets/" />
              <title>Generating Unique Random Numbers In JavaScript Using Sets</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Generating Unique Random Numbers In JavaScript Using Sets</h1>
                  
                    
                    <address>Amejimaobari Ollornwi</address>
                  
                  <time datetime="2024-08-26T15:00:00&#43;00:00" class="op-published">2024-08-26T15:00:00+00:00</time>
                  <time datetime="2024-08-26T15:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>JavaScript comes with a lot of built-in functions that allow you to carry out so many different operations. One of these built-in functions is the <code>Math.random()</code> method, which generates a random floating-point number that can then be manipulated into integers.</p>

<p>However, if you wish to generate a series of unique random numbers and create more random effects in your code, you will need to come up with a custom solution for yourself because the <code>Math.random()</code> method on its own cannot do that for you.</p>

<p>In this article, we’re going to be learning how to circumvent this issue and generate a series of unique random numbers using the <code>Set</code> object in JavaScript, which we can then use to create more randomized effects in our code.</p>

<p><strong>Note</strong>: <em>This article assumes that you know how to generate random numbers in JavaScript, as well as how to work with sets and arrays.</em></p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Roll up your sleeves and <strong>boost your UX skills</strong>! Meet <strong><a data-instant href="https://smart-interface-design-patterns.com/">Smart Interface Design Patterns</a></strong>&nbsp;🍣, a 10h video library by Vitaly Friedman. <strong>100s of real-life examples</strong> and live UX training. <a href="https://www.youtube.com/watch?v=3mwZztmGgbE">Free preview</a>.</p>
<a data-instant href="https://smart-interface-design-patterns.com/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://smart-interface-design-patterns.com/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3155f571-450d-42f9-81b4-494aa9b52841/video-course-smart-interface-design-patterns-vitaly-friedman.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8c98e7f9-8e62-4c43-b833-fc6bf9fea0a9/video-course-smart-interface-design-patterns-vitaly-friedman.jpg"
    alt="Feature Panel"
    width="690"
    height="790"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="generating-a-unique-series-of-random-numbers">Generating a Unique Series of Random Numbers</h2>

<p>One of the ways to generate a unique series of random numbers in JavaScript is by using <code>Set</code> objects. The reason why we’re making use of sets is because the elements of a set are unique. We can iteratively generate and insert random integers into sets until we get the number of integers we want.</p>

<p>And since sets do not allow duplicate elements, they are going to serve as a filter to remove all of the duplicate numbers that are generated and inserted into them so that we get a set of unique integers.</p>

<p>Here’s how we are going to approach the work:</p>

<ol>
<li>Create a <code>Set</code> object.</li>
<li>Define how many random numbers to produce and what range of numbers to use.</li>
<li>Generate each random number and immediately insert the numbers into the <code>Set</code> until the <code>Set</code> is filled with a certain number of them.</li>
</ol>

<p>The following is a quick example of how the code comes together:</p>

<div class="break-out">
<pre><code class="language-javascript">function generateRandomNumbers(count, min, max) {
  // 1: Create a `Set` object
  let uniqueNumbers = new Set();
  while (uniqueNumbers.size &lt; count) {
    // 2: Generate each random number
    uniqueNumbers.add(Math.floor(Math.random() &#42; (max - min + 1)) + min);
  }
  // 3: Immediately insert them numbers into the Set...
  return Array.from(uniqueNumbers);
}
// ...set how many numbers to generate from a given range
console.log(generateRandomNumbers(5, 5, 10));
</code></pre>
</div>

<p>What the code does is create a new <code>Set</code> object and then generate and add the random numbers to the set until our desired number of integers has been included in the set. The reason why we’re returning an array is because they are easier to work with.</p>

<p>One thing to note, however, is that the number of integers you want to generate (represented by <code>count</code> in the code) should be less than the upper limit of your range plus one (represented by <code>max + 1</code> in the code). Otherwise, the code will run forever. You can add an <code>if statement</code> to the code to ensure that this is always the case:</p>

<div class="break-out">
<pre><code class="language-javascript">function generateRandomNumbers(count, min, max) {
  // if statement checks that `count` is less than `max + 1`
  if (count &gt; max + 1) {
    return "count cannot be greater than the upper limit of range";
  } else {
    let uniqueNumbers = new Set();
    while (uniqueNumbers.size &lt; count) {
      uniqueNumbers.add(Math.floor(Math.random() &#42; (max - min + 1)) + min);
    }
    return Array.from(uniqueNumbers);
  }
}
console.log(generateRandomNumbers(5, 5, 10));
</code></pre>
</div>

<div class="partners__lead-place"></div>

<h2 id="using-the-series-of-unique-random-numbers-as-array-indexes">Using the Series of Unique Random Numbers as Array Indexes</h2>

<p>It is one thing to generate a series of random numbers. It’s another thing to use them.</p>

<p>Being able to use a series of random numbers with arrays unlocks so many possibilities: you can use them in shuffling playlists in a music app, randomly sampling data for analysis, or, as I did, <a href="https://github.com/jimavictor/remoji">shuffling the tiles in a memory game</a>.</p>

<p>Let’s take the code from the last example and work off of it to return random letters of the alphabet. First, we’ll construct an array of letters:</p>

<div class="break-out">
<pre><code class="language-javascript">const englishAlphabets = [
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 
  'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
];

// rest of code
</code></pre>
</div>

<p>Then we <code>map</code> the letters in the range of numbers:</p>

<div class="break-out">
<pre><code class="language-javascript">const englishAlphabets = [
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 
  'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
];

// generateRandomNumbers()

const randomAlphabets = randomIndexes.map((index) =&gt; englishAlphabets[index]);
</code></pre>
</div>

<p>In the original code, the <code>generateRandomNumbers()</code> function is logged to the console. This time, we’ll construct a new variable that calls the function so it can be consumed by <code>randomAlphabets</code>:</p>

<div class="break-out">
<pre><code class="language-javascript">const englishAlphabets = [
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 
  'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
];

// generateRandomNumbers()

const randomIndexes = generateRandomNumbers(5, 0, 25);
const randomAlphabets = randomIndexes.map((index) =&gt; englishAlphabets[index]);
</code></pre>
</div>

<p>Now we can log the output to the console like we did before to see the results:</p>

<div class="break-out">
<pre><code class="language-javascript">const englishAlphabets = [
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 
  'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
];

// generateRandomNumbers()

const randomIndexes = generateRandomNumbers(5, 0, 25);
const randomAlphabets = randomIndexes.map((index) =&gt; englishAlphabets[index]);
console.log(randomAlphabets);
</code></pre>
</div>

<p>And, when we put the <code>generateRandomNumbers</code><code>()</code> function definition back in, we get the final code:</p>

<div class="break-out">
<pre><code class="language-javascript">const englishAlphabets = [
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 
  'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
];
function generateRandomNumbers(count, min, max) {
  if (count &gt; max + 1) {
    return "count cannot be greater than the upper limit of range";
  } else {
    let uniqueNumbers = new Set();
    while (uniqueNumbers.size &lt; count) {
      uniqueNumbers.add(Math.floor(Math.random() &#42; (max - min + 1)) + min);
    }
    return Array.from(uniqueNumbers);
  }
}
const randomIndexes = generateRandomNumbers(5, 0, 25);
const randomAlphabets = randomIndexes.map((index) =&gt; englishAlphabets[index]);
console.log(randomAlphabets);
</code></pre>
</div>

<p>So, in this example, we created a new array of alphabets by randomly selecting some letters in our <code>englishAlphabets</code> array.</p>

<p>You can pass in a count argument of <code>englishAlphabets.length</code> to the <code>generateRandomNumbers</code> function if you desire to shuffle the elements in the <code>englishAlphabets</code> array instead. This is what I mean:</p>

<pre><code class="language-javascript">generateRandomNumbers(englishAlphabets.length, 0, 25);
</code></pre>

<div class="partners__lead-place"></div>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>In this article, we’ve discussed how to create randomization in JavaScript by covering how to generate a series of unique random numbers, how to use these random numbers as indexes for arrays, and also some practical applications of randomization.</p>

<p>The best way to learn anything in software development is by consuming content and reinforcing whatever knowledge you’ve gotten from that content by practicing. So, don’t stop here. Run the examples in this tutorial (if you haven’t done so), play around with them, come up with your own unique solutions, and also don’t forget to share your good work. Ciao!</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Steven Levithan</author><title>Regexes Got Good: The History And Future Of Regular Expressions In JavaScript</title><link>https://www.smashingmagazine.com/2024/08/history-future-regular-expressions-javascript/</link><pubDate>Tue, 20 Aug 2024 15:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/08/history-future-regular-expressions-javascript/</guid><description>Although JavaScript regexes used to be underpowered compared to other modern flavors, numerous improvements in recent years mean that’s no longer true. Steven Levithan evaluates the history and present state of regular expressions in JavaScript with tips to make your regexes more readable, maintainable, and resilient.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/08/history-future-regular-expressions-javascript/" />
              <title>Regexes Got Good: The History And Future Of Regular Expressions In JavaScript</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Regexes Got Good: The History And Future Of Regular Expressions In JavaScript</h1>
                  
                    
                    <address>Steven Levithan</address>
                  
                  <time datetime="2024-08-20T15:00:00&#43;00:00" class="op-published">2024-08-20T15:00:00+00:00</time>
                  <time datetime="2024-08-20T15:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Modern JavaScript regular expressions have come a long way compared to what you might be familiar with. Regexes can be <strong>an amazing tool for searching and replacing text</strong>, but they have a longstanding reputation (perhaps outdated, as I’ll show) for being difficult to write and understand.</p>

<p>This is especially true in JavaScript-land, where regexes languished for many years, comparatively underpowered compared to their more modern counterparts in PCRE, Perl, .NET, Java, Ruby, C++, and Python. Those days are over.</p>

<p>In this article, I’ll recount the history of improvements to JavaScript regexes (spoiler: ES2018 and ES2024 changed the game), show examples of modern regex features in action, introduce you to a lightweight <a href="https://github.com/slevithan/regex">JavaScript library</a> that makes JavaScript stand alongside or surpass other modern regex flavors, and end with a preview of active proposals that will continue to improve regexes in future versions of JavaScript (with some of them already working in your browser today).</p>

<h2 id="the-history-of-regular-expressions-in-javascript">The History of Regular Expressions in JavaScript</h2>

<p>ECMAScript 3, standardized in 1999, introduced Perl-inspired regular expressions to the JavaScript language. Although it got enough things right to make regexes pretty useful (and mostly compatible with other Perl-inspired flavors), there were some big omissions, even then. And while JavaScript waited 10 years for its next standardized version with ES5, other programming languages and regex implementations added useful new features that made their regexes more powerful and readable.</p>

<p>But that was then.</p>

<blockquote>Did you know that nearly every new version of JavaScript has made at least minor improvements to regular expressions?</blockquote>

<p>Let’s take a look at them.</p>

<p>Don’t worry if it’s hard to understand what some of the following features mean &mdash; we’ll look more closely at several of the key features afterward.</p>

<ul>
<li>ES5 (2009) fixed unintuitive behavior by creating a new object every time regex literals are evaluated and allowed regex literals to use unescaped forward slashes within character classes (<code>/[/]/</code>).</li>
<li>ES6/ES2015 added two new regex flags: <code>y</code> (<code>sticky</code>), which made it easier to use regexes in parsers, and <code>u</code> (<code>unicode</code>), which added several significant Unicode-related improvements along with strict errors. It also added the <code>RegExp.prototype.flags</code> getter, support for subclassing <code>RegExp</code>, and the ability to copy a regex while changing its flags.</li>
<li>ES2018 was the edition that finally made JavaScript regexes pretty good. It added the <code>s</code> (<code>dotAll</code>) flag, lookbehind, named capture, and Unicode properties (via <code>\p{...}</code> and <code>\P{...}</code>, which require ES6’s flag <code>u</code>). All of these are extremely useful features, as we’ll see.</li>
<li>ES2020 added the string method <code>matchAll</code>, which we’ll also see more of shortly.</li>
<li>ES2022 added flag <code>d</code> (<code>hasIndices</code>), which provides start and end indices for matched substrings.</li>
<li>And finally, ES2024 added flag <code>v</code> (<code>unicodeSets</code>) as an upgrade to ES6’s flag <code>u</code>. The <code>v</code> flag adds a set of multicharacter “properties of strings” to <code>\p{...}</code>, multicharacter elements within character classes via <code>\p{...}</code> and <code>\q{...}</code>, nested character classes, set subtraction <code>[A--B]</code> and intersection <code>[A&amp;&amp;B]</code>, and different escaping rules within character classes. It also fixed case-insensitive matching for Unicode properties within negated sets <code>[^...]</code>.</li>
</ul>

<p class="c-pre-sidenote--left">As for whether you can safely use these features in your code today, the answer is yes! The latest of these features, flag <code>v</code>, is supported in Node.js 20 and <a href="https://caniuse.com/mdn-javascript_builtins_regexp_unicodesets">2023-era</a> browsers. The rest are supported in 2021-era browsers or earlier.</p>
<p class="c-sidenote c-sidenote--right">Each edition from ES2019 to ES2023 also added additional Unicode properties that can be used via <code>\p{...}</code> and <code>\P{...}</code>. And to be a completionist, ES2021 added string method <code>replaceAll</code> &mdash; although, when given a regex, the only difference from ES3’s <code>replace</code> is that it throws if not using flag <code>g</code>.</p>

<h3 id="aside-what-makes-a-regex-flavor-good">Aside: What Makes a Regex Flavor Good?</h3>

<p>With all of these changes, how do JavaScript regular expressions now stack up against other flavors? There are multiple ways to think about this, but here are a few key aspects:</p>

<ul>
<li><strong>Performance.</strong><br />
This is an important aspect but probably not the main one since mature regex implementations are generally pretty fast. JavaScript is strong on regex performance (at least considering V8’s Irregexp engine, used by Node.js, Chromium-based browsers, and <a href="https://hacks.mozilla.org/2020/06/a-new-regexp-engine-in-spidermonkey/">even Firefox</a>; and JavaScriptCore, used by Safari), but it uses a backtracking engine that is missing any syntax for backtracking control &mdash; a major limitation that makes ReDoS vulnerability more common.</li>
<li><strong>Support for advanced features</strong> that handle common or important use cases.<br />
Here, JavaScript stepped up its game with ES2018 and ES2024. JavaScript is now best in class for some features like lookbehind (with its infinite-length support) and Unicode properties (with multicharacter “properties of strings,” set subtraction and intersection, and script extensions). These features are either not supported or not as robust in many other flavors.</li>
<li><strong>Ability to write readable and maintainable patterns.</strong><br />
Here, native JavaScript has long been the worst of the major flavors since it lacks the <code>x</code> (“extended”) flag that allows insignificant whitespace and comments. Additionally, it lacks regex subroutines and subroutine definition groups (from PCRE and Perl), a powerful set of features that enable writing grammatical regexes that build up complex patterns via composition.</li>
</ul>

<p>So, it’s a bit of a mixed bag.</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aJavaScript%20regexes%20have%20become%20exceptionally%20powerful,%20but%20they%e2%80%99re%20still%20missing%20key%20features%20that%20could%20make%20regexes%20safer,%20more%20readable,%20and%20more%20maintainable%20%28all%20of%20which%20hold%20some%20people%20back%20from%20using%20this%20power%29.%0a&url=https://smashingmagazine.com%2f2024%2f08%2fhistory-future-regular-expressions-javascript%2f">
      
JavaScript regexes have become exceptionally powerful, but they’re still missing key features that could make regexes safer, more readable, and more maintainable (all of which hold some people back from using this power).

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>The good news is that all of these holes can be filled by a JavaScript library, which we’ll see later in this article.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p><p>Meet <a data-instant href="/the-smashing-newsletter/"><strong>Smashing Email Newsletter</strong></a> with useful tips on front-end, design &amp; UX. Subscribe and <strong>get “Smart Interface Design Checklists”</strong> &mdash; a <strong>free PDF deck</strong> with 150+ questions to ask yourself when designing and building almost <em>anything</em>.</p><div><section class="nlbf"><form action="//smashingmagazine.us1.list-manage.com/subscribe/post?u=16b832d9ad4b28edf261f34df&amp;id=a1666656e0" method="post"><div class="nlbwrapper"><label for="mce-EMAIL-hp" class="sr-only">Your (smashing) email</label><div class="nlbgroup"><input type="email" name="EMAIL" class="nlbf-email" id="mce-EMAIL-hp" placeholder="Your email">
<input type="submit" value="Meow!" name="subscribe" class="nlbf-button"></div></div></form><style>.c-garfield-the-cat .nlbwrapper{margin-bottom: 0;}.nlbf{display:flex;padding-bottom:.25em;padding-top:.5em;text-align:center;letter-spacing:-.5px;color:#fff;font-size:1.15em}.nlbgroup:hover{box-shadow:0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025)}.nlbf .nlbf-button,.nlbf .nlbf-email{flex-grow:1;flex-shrink:0;width:auto;margin:0;padding:.75em 1em;border:0;border-radius:11px;background:#fff;font-size:1em;box-shadow:none}.promo-box .nlbf-button:focus,.promo-box input.nlbf-email:active,.promo-box input.nlbf-email:focus{box-shadow:none}.nlbf-button:-ms-input-placeholder,.nlbf-email:-ms-input-placeholder{color:#777;font-style:italic}.nlbf-button::-webkit-input-placeholder,.nlbf-email::-webkit-input-placeholder{color:#777;font-style:italic}.nlbf-button:-ms-input-placeholder,.nlbf-button::-moz-placeholder,.nlbf-button::placeholder,.nlbf-email:-ms-input-placeholder,.nlbf-email::-moz-placeholder,.nlbf-email::placeholder{color:#777;font-style:italic}.nlbf .nlbf-button{transition:all .2s ease-in-out;color:#fff;background-color:#0168b8;font-weight:700;box-shadow:0 1px 1px rgba(0,0,0,.3);width:100%;border:0;border-left:1px solid #ddd;flex:2;border-top-left-radius:0;border-bottom-left-radius:0}.nlbf .nlbf-email{border-top-right-radius:0;border-bottom-right-radius:0;width:100%;flex:4;min-width:150px}@media all and (max-width:650px){.nlbf .nlbgroup{flex-wrap:wrap;box-shadow:none}.nlbf .nlbf-button,.nlbf .nlbf-email{border-radius:11px;border-left:none}.nlbf .nlbf-email{box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);min-width:100%}.nlbf .nlbf-button{margin-top:1em;box-shadow:0 1px 1px rgba(0,0,0,.5)}}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus,.nlbf .nlbf-button:hover{cursor:pointer;color:#fff;background-color:#0168b8;border-color:#dadada;box-shadow:0 1px 1px rgba(0,0,0,.3)}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus{outline:0!important;text-shadow:1px 1px 1px rgba(0,0,0,.3);box-shadow:inset 0 3px 3px rgba(0,0,0,.3)}.nlbgroup{display:flex;box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);border-radius:11px;transition:box-shadow .2s ease-in-out}.nlbwrapper{display:flex;flex-direction:column;justify-content:center}.nlbf form{width:100%}.nlbf .nlbgroup{margin:0}.nlbcaption{font-size:.9em;line-height:1.5em;color:#fff;border-radius:11px;padding:.5em 1em;display:inline-block;background-color:#0067b859;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.wf-loaded-stage2 .nlbf .nlbf-button{font-family:Mija}.mts{margin-top: 5px !important;}.mbn{margin-bottom: 0 !important;}</style></section><p class="mts mbn"><small class="promo-box__footer mtm block grey"><em>Once a week. Useful tips on <a href="https://www.smashingmagazine.com/the-smashing-newsletter/">front-end &amp; UX</a>. Trusted by 207.000 friendly folks.</em></small></p></div></p>
</div>
</div>
<div class="feature-panel-right-col">
<div class="feature-panel-image">
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="/images/smashing-cat/cat-firechat.svg"
    alt="Feature Panel"
    width="310"
    height="400"
/>

</div>

<p></div>
</aside>
</div></p>

<h2 id="using-javascript-s-modern-regex-features">Using JavaScript’s Modern Regex Features</h2>

<p>Let’s look at a few of the more useful modern regex features that you might be less familiar with. You should know in advance that this is <strong>a moderately advanced guide</strong>. If you’re relatively new to regex, here are some excellent tutorials you might want to start with:</p>

<ul>
<li><a href="https://regexlearn.com/">RegexLearn</a> and <a href="https://regexone.com/">RegexOne</a> are interactive tutorials that include practice problems.</li>
<li>JavaScript.info’s <a href="https://javascript.info/regular-expressions">regular expressions</a> chapter is a detailed and JavaScript-specific guide.</li>
<li><a href="https://www.youtube.com/watch?v=M7vDtxaD7ZU">Demystifying Regular Expressions</a> (video) is an excellent presentation for beginners by Lea Verou at HolyJS 2017.</li>
<li><a href="https://www.youtube.com/watch?v=rhzKDrUiJVk">Learn Regular Expressions In 20 Minutes</a> (video) is a live syntax walkthrough in a regex tester.</li>
</ul>

<h3 id="named-capture">Named Capture</h3>

<p>Often, you want to do more than just check whether a regex matches &mdash; you want to extract substrings from the match and do something with them in your code. Named capturing groups allow you to do this in a way that makes your regexes and code <strong>more readable</strong> and <strong>self-documenting</strong>.</p>

<p>The following example matches a record with two date fields and captures the values:</p>

<div class="break-out">
<pre><code class="language-javascript">const record = 'Admitted: 2024-01-01\nReleased: 2024-01-03';
const re = /^Admitted: (?&lt;admitted&gt;\d{4}-\d{2}-\d{2})\nReleased: (?&lt;released&gt;\d{4}-\d{2}-\d{2})$/;
const match = record.match(re);
console.log(match.groups);
/&#42; → {
  admitted: '2024-01-01',
  released: '2024-01-03'
} &#42;/
</code></pre>
</div>

<p>Don’t worry &mdash; although this regex might be challenging to understand, later, we’ll look at a way to make it much more readable. The key things here are that named capturing groups use the syntax <code>(?&lt;name&gt;...)</code>, and their results are stored on the <code>groups</code> object of matches.</p>

<p>You can also use named backreferences to rematch whatever a named capturing group matched via <code>\k&lt;name&gt;</code>, and you can use the values within search and replace as follows:</p>

<pre><code class="language-javascript">// Change 'FirstName LastName' to 'LastName, FirstName'
const name = 'Shaquille Oatmeal';
name.replace(/(?&lt;first&gt;\w+) (?&lt;last&gt;\w+)/, '$&lt;last&gt;, $&lt;first&gt;');
// → 'Oatmeal, Shaquille'
</code></pre>

<p>For advanced regexers who want to use named backreferences within a replacement callback function, the <code>groups</code> object is provided as the last argument. Here’s a fancy example:</p>

<pre><code class="language-javascript">function fahrenheitToCelsius(str) {
  const re = /(?&lt;degrees&gt;-?\d+(\.\d+)?)F\b/g;
  return str.replace(re, (...args) =&gt; {
    const groups = args.at(-1);
    return Math.round((groups.degrees - 32) &#42; 5/9) + 'C';
  });
}
fahrenheitToCelsius('98.6F');
// → '37C'
fahrenheitToCelsius('May 9 high is 40F and low is 21F');
// → 'May 9 high is 4C and low is -6C'
</code></pre>

<h3 id="lookbehind">Lookbehind</h3>

<p>Lookbehind (introduced in ES2018) is the complement to <em>lookahead</em>, which has always been supported by JavaScript regexes. Lookahead and lookbehind are <em>assertions</em> (similar to <code>^</code> for the start of a string or <code>\b</code> for word boundaries) that don’t consume any characters as part of the match. Lookbehinds succeed or fail based on whether their subpattern can be found immediately before the current match position.</p>

<p>For example, the following regex uses a lookbehind <code>(?&lt;=...)</code> to match the word “cat” (<em>only</em> the word “cat”) if it’s preceded by “fat ”:</p>

<pre><code class="language-javascript">const re = /(?&lt;=fat )cat/g;
'cat, fat cat, brat cat'.replace(re, 'pigeon');
// → 'cat, fat pigeon, brat cat'
</code></pre>

<p>You can also use <em>negative</em> lookbehind &mdash; written as <code>(?&lt;!...)</code> &mdash; to invert the assertion. That would make the regex match any instance of “cat” that’s <em>not</em> preceded by “fat ”.</p>

<pre><code class="language-javascript">const re = /(?&lt;!fat )cat/g;
'cat, fat cat, brat cat'.replace(re, 'pigeon');
// → 'pigeon, fat cat, brat pigeon'
</code></pre>

<p>JavaScript’s implementation of lookbehind is one of the very best (matched only by .NET). Whereas other regex flavors have inconsistent and complex rules for when and whether they allow variable-length patterns inside lookbehind, JavaScript allows you to look behind for any subpattern.</p>

<h3 id="the-matchall-method">The <code>matchAll</code> Method</h3>

<p>JavaScript’s <code>String.prototype.matchAll</code> was added in ES2020 and makes it easier to operate on regex matches in a loop when you need extended match details. Although other solutions were possible before, <code>matchAll</code> is often easier, and it avoids gotchas, such as the need to guard against infinite loops when looping over the results of regexes that might return zero-length matches.</p>

<p>Since <code>matchAll</code> returns an iterator (rather than an array), it’s easy to use it in a <code>for...of</code> loop.</p>

<div class="break-out">
<pre><code class="language-javascript">const re = /(?&lt;char1&gt;\w)(?&lt;char2&gt;\w)/g;
for (const match of str.matchAll(re)) {
  const {char1, char2} = match.groups;
  // Print each complete match and matched subpatterns
  console.log(`Matched "${match[0]}" with "${char1}" and "${char2}"`);
}
</code></pre>
</div>

<p><strong>Note</strong>: <em><code>matchAll</code> requires its regexes to use flag <code>g</code> (<code>global</code>). Also, as with other iterators, you can get all of its results as an array using <code>Array.from</code> or array spreading.</em></p>

<pre><code class="language-javascript">const matches = [...str.matchAll(/./g)];
</code></pre>

<h3 id="unicode-properties">Unicode Properties</h3>

<p>Unicode properties (added in ES2018) give you powerful control over multilingual text, using the syntax <code>\p{...}</code> and its negated version <code>\P{...}</code>. There are hundreds of different properties you can match, which cover a wide variety of Unicode categories, scripts, script extensions, and binary properties.</p>

<p><strong>Note</strong>: <em>For more details, check out the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape">documentation on MDN</a>.</em></p>

<p>Unicode properties require using the flag <code>u</code> (<code>unicode</code>) or <code>v</code> (<code>unicodeSets</code>).</p>

<h3 id="flag-v">Flag <code>v</code></h3>

<p>Flag <code>v</code> (<code>unicodeSets</code>) was added in ES2024 and is an upgrade to flag <code>u</code> &mdash; you can’t use both at the same time. It’s a best practice to always use one of these flags to avoid silently introducing bugs via the default Unicode-unaware mode. The decision on which to use is fairly straightforward. If you’re okay with only supporting environments with flag <code>v</code> (Node.js 20 and 2023-era browsers), then use flag <code>v</code>; otherwise, use flag <code>u</code>.</p>

<p>Flag <code>v</code> adds support for several new regex features, with the coolest probably being set subtraction and intersection. This allows using <code>A--B</code> (within character classes) to match strings in <em>A</em> but not in <em>B</em> or using <code>A&amp;&amp;B</code> to match strings in both <em>A</em> and <em>B</em>. For example:</p>

<pre><code class="language-javascript">// Matches all Greek symbols except the letter 'π'
/[\p{Script&#95;Extensions=Greek}--π]/v

// Matches only Greek letters
/[\p{Script&#95;Extensions=Greek}&&\p{Letter}]/v
</code></pre>

<p>For more details about flag <code>v</code>, including its other new features, check out this <a href="https://v8.dev/features/regexp-v-flag">explainer</a> from the Google Chrome team.</p>

<h4 id="a-word-on-matching-emoji">A Word on Matching Emoji</h4>

<p>Emoji are 🤩🔥😎👌, but how emoji get encoded in text is complicated. If you’re trying to match them with a regex, it’s important to be aware that <strong>a single emoji can be composed of one or many individual Unicode code points</strong>. Many people (and libraries!) who roll their own emoji regexes miss this point (or implement it poorly) and end up with bugs.</p>

<p>The following details for the emoji “👩🏻‍🏫” (<em>Woman Teacher: Light Skin Tone</em>) show just how complicated emoji can be:</p>

<div class="break-out">
<pre><code class="language-javascript">// Code unit length
'👩🏻‍🏫'.length;
// → 7
// Each astral code point (above \uFFFF) is divided into high and low surrogates

// Code point length
[...'👩🏻‍🏫'].length;
// → 4
// These four code points are: \u{1F469} \u{1F3FB} \u{200D} \u{1F3EB}
// \u{1F469} combined with \u{1F3FB} is '👩🏻'
// \u{200D} is a Zero-Width Joiner
// \u{1F3EB} is '🏫'

// Grapheme cluster length (user-perceived characters)
[...new Intl.Segmenter().segment('👩🏻‍🏫')].length;
// → 1
</code></pre>
</div>

<p>Fortunately, JavaScript added an easy way to match any individual, complete emoji via <code>\p{RGI_Emoji}</code>. Since this is a fancy “property of strings” that can match more than one code point at a time, it requires ES2024’s flag <code>v</code>.</p>

<p>If you want to match emojis in environments without <code>v</code> support, check out the excellent libraries <a href="https://github.com/mathiasbynens/emoji-regex">emoji-regex</a> and <a href="https://github.com/slevithan/emoji-regex-xs">emoji-regex-xs</a>.</p>

<div class="partners__lead-place"></div>

<h2 id="making-your-regexes-more-readable-maintainable-and-resilient">Making Your Regexes More Readable, Maintainable, and Resilient</h2>

<p>Despite the improvements to regex features over the years, native JavaScript regexes of sufficient complexity can still be outrageously hard to read and maintain.</p>

<p><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Regular Expressions are SO EASY!!!! <a href="https://t.co/q4GSpbJRbZ">pic.twitter.com/q4GSpbJRbZ</a></p>&mdash; Garabato Kid (@garabatokid) <a href="https://twitter.com/garabatokid/status/1147063121678389253?ref_src=twsrc%5Etfw">July 5, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p>

<p><br/>
ES2018’s named capture was a great addition that made regexes more self-documenting, and ES6’s <code>String.raw</code> tag allows you to avoid escaping all your backslashes when using the <code>RegExp</code> constructor. But for the most part, that’s it in terms of readability.</p>

<p class="c-pre-sidenote--left">However, there’s a lightweight and high-performance <a href="https://github.com/slevithan/regex">JavaScript library</a> named <code>regex</code> (by yours truly) that makes regexes dramatically more readable. It does this by adding key missing features from Perl-Compatible Regular Expressions (PCRE) and outputting native JavaScript regexes. You can also use it as a Babel plugin, which means that <code>regex</code> calls are transpiled at build time, so you get a better developer experience without users paying any runtime cost.</p>
<p class="c-sidenote c-sidenote--right"><a href="https://github.com/PCRE2Project/pcre2">PCRE</a> is a popular C library used by PHP for its regex support, and it’s available in countless other programming languages and tools.</p>

<p>Let’s briefly look at some of the ways the <code>regex</code> library, which provides a template tag named <code>regex</code>, can help you write complex regexes that are actually understandable and maintainable by mortals. Note that all of the new syntax described below works identically in PCRE.</p>

<h3 id="insignificant-whitespace-and-comments">Insignificant Whitespace and Comments</h3>

<p>By default, <code>regex</code> allows you to freely add whitespace and line comments (starting with <code>#</code>) to your regexes for readability.</p>

<pre><code class="language-javascript">import {regex} from 'regex';
const date = regex`
  &#35; Match a date in YYYY-MM-DD format
  (?&lt;year&gt;  \d{4}) - &#35; Year part
  (?&lt;month&gt; \d{2}) - &#35; Month part
  (?&lt;day&gt;   \d{2})   &#35; Day part
`;
</code></pre>
  

<p>This is equivalent to using PCRE’s <code>xx</code> flag.</p>

<h3 id="subroutines-and-subroutine-definition-groups">Subroutines and Subroutine Definition Groups</h3>

<p>Subroutines are written as <code>\g&lt;name&gt;</code> (where <em>name</em> refers to a named group), and they treat the referenced group as an independent subpattern that they try to match at the current position. This enables subpattern composition and reuse, which improves readability and maintainability.</p>

<p>For example, the following regex matches an IPv4 address such as “192.168.12.123”:</p>

<pre><code class="language-javascript">import {regex} from 'regex';
const ipv4 = regex`\b
  (?&lt;byte&gt; 25[0-5] | 2[0-4]\d | 1\d\d | [1-9]?\d)
  &#35; Match the remaining 3 dot-separated bytes
  (\. \g&lt;byte&gt;){3}
\b`;
</code></pre>

<p>You can take this even further by defining subpatterns for use by reference only via subroutine definition groups. Here’s an example that improves the regex for admittance records that we saw earlier in this article:</p>

<pre><code class="language-javascript">const record = 'Admitted: 2024-01-01\nReleased: 2024-01-03';
const re = regex`
  ^ Admitted:\ (?&lt;admitted&gt; \g&lt;date&gt;) \n
    Released:\ (?&lt;released&gt; \g&lt;date&gt;) $

  (?(DEFINE)
    (?&lt;date&gt;  \g&lt;year&gt;-\g&lt;month&gt;-\g&lt;day&gt;)
    (?&lt;year&gt;  \d{4})
    (?&lt;month&gt; \d{2})
    (?&lt;day&gt;   \d{2})
  )
`;
const match = record.match(re);
console.log(match.groups);
/&#42; → {
  admitted: '2024-01-01',
  released: '2024-01-03'
} &#42;/
</code></pre>

<h3 id="a-modern-regex-baseline">A Modern Regex Baseline</h3>

<p><code>regex</code> includes the <code>v</code> flag by default, so you never forget to turn it on. And in environments without native <code>v</code>, it automatically switches to flag <code>u</code> while applying <code>v</code>’s escaping rules, so your regexes are forward and backward-compatible.</p>

<p>It also implicitly enables the emulated flags <code>x</code> (insignificant whitespace and comments) and <code>n</code> (“named capture only” mode) by default, so you don’t have to continually opt into their superior modes. And since it’s a raw string template tag, you don’t have to escape your backslashes <code>\\\\</code> like with the <code>RegExp</code> constructor.</p>

<h3 id="atomic-groups-and-possessive-quantifiers-can-prevent-catastrophic-backtracking">Atomic Groups and Possessive Quantifiers Can Prevent Catastrophic Backtracking</h3>

<p>Atomic groups and possessive quantifiers are another powerful set of features added by the <code>regex</code> library. Although they’re primarily about performance and resilience against catastrophic backtracking (also known as ReDoS or “regular expression denial of service,” a serious issue where certain regexes can take forever when searching particular, not-quite-matching strings), they can also help with readability by allowing you to write simpler patterns.</p>

<p><strong>Note</strong>: <em>You can learn more in the <code>regex</code> <a href="https://github.com/slevithan/regex#atomic-groups">documentation</a>.</em></p>

<div class="partners__lead-place"></div>

<h2 id="what-s-next-upcoming-javascript-regex-improvements">What’s Next? Upcoming JavaScript Regex Improvements</h2>

<p>There are a variety of active proposals for improving regexes in JavaScript. Below, we’ll look at the three that are well on their way to being included in future editions of the language.</p>

<h3 id="duplicate-named-capturing-groups">Duplicate Named Capturing Groups</h3>

<p>This is a Stage 3 (nearly finalized) <a href="https://github.com/tc39/proposal-duplicate-named-capturing-groups">proposal</a>. Even better is that, as of recently, it works in all major browsers.</p>

<p>When named capturing was first introduced, it required that all <code>(?&lt;name&gt;...)</code> captures use unique names. However, there are cases when you have multiple alternate paths through a regex, and it would simplify your code to reuse the same group names in each alternative.</p>

<p>For example:</p>

<pre><code class="language-javascript">/(?&lt;year&gt;\d{4})-\d\d|\d\d-(?&lt;year&gt;\d{4})/
</code></pre>

<p>This proposal enables exactly this, preventing a “duplicate capture group name” error with this example. Note that names must still be unique <em>within</em> each alternative path.</p>

<h3 id="pattern-modifiers-aka-flag-groups">Pattern Modifiers (aka Flag Groups)</h3>

<p>This is another Stage 3 <a href="https://github.com/tc39/proposal-regexp-modifiers">proposal</a>. It’s already supported in Chrome/Edge 125 and Opera 111, and it’s coming <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1899813">soon</a> for Firefox. No word <a href="https://bugs.webkit.org/show_bug.cgi?id=275672">yet</a> on Safari.</p>

<p>Pattern modifiers use <code>(?ims:...)</code>, <code>(?-ims:...)</code>, or <code>(?im-s:...)</code> to turn the flags <code>i</code>, <code>m</code>, and <code>s</code> on or off for only certain parts of a regex.</p>

<p>For example:</p>

<pre><code class="language-javascript">/hello-(?i:world)/
// Matches 'hello-WORLD' but not 'HELLO-WORLD'
</code></pre>

<h3 id="escape-regex-special-characters-with-regexp-escape">Escape Regex Special Characters with <code>RegExp.escape</code></h3>

<p>This <a href="https://github.com/tc39/proposal-regex-escaping">proposal</a> recently reached Stage 3 and has been a long time coming. It isn’t yet supported in any major browsers. The proposal does what it says on the tin, providing the function <code>RegExp.escape(str)</code>, which returns the string with all regex special characters escaped so you can match them literally.</p>

<p>If you need this functionality today, the most widely-used package (with more than 500 million monthly npm downloads) is <a href="https://github.com/sindresorhus/escape-string-regexp">escape-string-regexp</a>, an ultra-lightweight, single-purpose utility that does minimal escaping. That’s great for most cases, but if you need assurance that your escaped string can safely be used at any arbitrary position within a regex, <code>escape-string-regexp</code> recommends the <code>regex</code> library that we’ve already looked at in this article. The <code>regex</code> library uses interpolation to escape embedded strings in a <a href="https://github.com/slevithan/regex#interpolating-escaped-strings">context-aware way</a>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>So there you have it: the past, present, and future of JavaScript regular expressions.</p>

<p>If you want to journey even deeper into the lands of regex, check out <a href="https://github.com/slevithan/awesome-regex">Awesome Regex</a> for a list of the best regex testers, tutorials, libraries, and other resources. And for a fun regex crossword puzzle, try your hand at <a href="https://regexle.com/">regexle</a>.</p>

<p>May your parsing be prosperous and your regexes be readable.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Tim Benniks</author><title>How To Build A Multilingual Website With Nuxt.js</title><link>https://www.smashingmagazine.com/2024/08/how-build-multilingual-website-nuxt-i18n/</link><pubDate>Thu, 01 Aug 2024 15:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/08/how-build-multilingual-website-nuxt-i18n/</guid><description>Handling translations for multilingual websites is famously difficult and, yet, crucial for many companies and organizations that serve a global audience. Thankfully, modern tooling abstracts away a great deal of the work, allowing for seamless translations that dynamically update the rendered content on a page, as demonstrated in this step-by-step tutorial.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/08/how-build-multilingual-website-nuxt-i18n/" />
              <title>How To Build A Multilingual Website With Nuxt.js</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>How To Build A Multilingual Website With Nuxt.js</h1>
                  
                    
                    <address>Tim Benniks</address>
                  
                  <time datetime="2024-08-01T15:00:00&#43;00:00" class="op-published">2024-08-01T15:00:00+00:00</time>
                  <time datetime="2024-08-01T15:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                <p>This article is sponsored by <b>Hygraph</b></p>
                

<p>Internationalization, often abbreviated as i18n, is the process of designing and developing software applications in a way that they can be easily adapted to various spoken languages like English, German, French, and more without requiring substantial changes to the codebase. It involves <strong>moving away from hardcoded strings and techniques</strong> for translating text, formatting dates and numbers, and handling different character encodings, among other tasks.</p>

<p>Internationalization can <strong>give users the choice</strong> to access a given website or application in their native language, which can have a positive impression on them, making it crucial for reaching a global audience.</p>

<h2 id="what-we-re-making">What We’re Making</h2>

<p>In this tutorial, we’re making a website that puts these i18n pieces together using a combination of libraries and a UI framework. You’ll want to have intermediate proficiency with JavaScript, Vue, and Nuxt to follow along. Throughout this article, we will <strong>learn by examples</strong> and incrementally <strong>build a multilingual Nuxt website</strong>. Together, we will learn how to provide i18n support for different languages, lazy-load locale messages, and switch locale on runtime.</p>

<p>After that, we will explore features like interpolation, pluralization, and date/time translations.</p>

<p>And finally, we will fetch dynamic localized content from an API server using Hygraph as our API server to get localized content. If you do not have a Hygraph account <a href="https://hygraph.com/">please create one for free</a> before jumping in.</p>

<p>As a final detail, we will use <a href="https://vuetifyjs.com/en/">Vuetify</a> as our UI framework, but please feel free to use another framework if you want. The final code for what we’re building is published in a <a href="https://github.com/hygraph/hygraph-examples/tree/master/with-nuxtjs-i18n">GitHub repository</a> for reference. And finally, you can also take a look at the final result <a href="https://nuxtjs-i18n.withheadlesscms.com/">in a live demo</a>.</p>

<h2 id="the-nuxt-i18n-library">The <code>nuxt-i18n</code> Library</h2>

<p><a href="https://i18n.nuxtjs.org/"><code>nuxt-i18n</code></a> is a library for implementing internationalization in Nuxt.js applications, and it’s what we will be using in this tutorial. The library is built on top of <a href="https://vue-i18n.intlify.dev/">Vue I18n</a>, which, again, is the de facto standard library for implementing i18n in Vue applications.</p>

<p>What makes <code>nuxt-i18n</code> ideal for our work is that it provides the comprehensive set of features included in Vue I18n while adding more functionalities that are specific to Nuxt, like lazy loading locale messages, route generation and redirection for different locales, SEO metadata per locale, locale-specific domains, and more.</p>

<h2 id="initial-setup">Initial Setup</h2>

<p>Start a new Nuxt.js project and set it up with a UI framework of your choice. Again, I will be using Vue to establish the interface for this tutorial.</p>

<p>Let us add a basic layout for our website and set up some sample Vue templates.</p>

<p>First, a “Blog” page:</p>

<pre><code class="language-html">&lt;!-- pages/blog.vue --&gt;
&lt;template&gt;
  &lt;div&gt;
    &lt;v-card color="cardBackground"&gt;
      &lt;v-card-title class="text-overline"&gt;
        Home
      &lt;/v-card-title&gt;
      &lt;v-card-text&gt;
        This is the home page description
      &lt;/v-card-text&gt;
    &lt;/v-card&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>

<p>Next, an “About” page:</p>

<pre><code class="language-html">&lt;!-- pages/about.vue --&gt;
&lt;template&gt;
  &lt;div&gt;
    &lt;v-card color="cardBackground"&gt;
      &lt;v-card-title class="text-overline"&gt;
        About
      &lt;/v-card-title&gt;
      &lt;v-card-text&gt;
        This is the about page description
      &lt;/v-card-text&gt;
    &lt;/v-card&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>

<p>This gives us a bit of a boilerplate that we can integrate our i18n work into.</p>

<h2 id="translating-plain-text">Translating Plain Text</h2>

<p>The page templates look good, but notice how the text is hardcoded. As far as i18n goes, hardcoded content is difficult to translate into different locales. That is where the <code>nuxt-i18n</code> library comes in, providing the language-specific strings we need for the Vue components in the templates.</p>

<p>We’ll start by installing the library via the command line:</p>

<pre><code class="language-bash">npx nuxi@latest module add i18n
</code></pre>

<p>Inside the <code>nuxt.config.ts</code> file, we need to ensure that we have <code>@nuxtjs/i18n</code> inside the <code>modules</code> array. We can use the <code>i18n</code> property to provide module-specific configurations.</p>

<pre><code class="language-javascript">// nuxt.config.ts
export default defineNuxtConfig({
  // ...
  modules: [
    ...
    "@nuxtjs/i18n",
    // ...
  ],
  i18n: {
    // nuxt-i18n module configurations here
  }
  // ...
});
</code></pre>

<p>Since the <code>nuxt-i18n</code> library is built on top of the Vue I18n library, we can utilize its features in our Nuxt application as well. Let us create a new file, <code>i18n.config.ts</code>, which we will use to provide all <code>vue-i18n</code> configurations.</p>

<pre><code class="language-javascript">// i18n.config.ts
export default defineI18nConfig(() =&gt; ({
  legacy: false,
  locale: "en",
  messages: {
    en: {
      homePage: {
        title: "Home",
        description: "This is the home page description."
      },
      aboutPage: {
        title: "About",
        description: "This is the about page description."
      },
    },
  },
}));
</code></pre>

<p>Here, we have specified internationalization configurations, like using the <code>en</code> locale, and added messages for the <code>en</code> locale. These messages can be used inside the markup in the templates we made with the help of a <code>$t</code> function from Vue I18n.</p>

<p>Next, we need to link the <code>i18n.config.ts</code> configurations in our Nuxt config file.</p>

<pre><code class="language-javascript">// nuxt.config.ts
export default defineNuxtConfig({
  ...
  i18n: {
    vueI18n: "./i18n.config.ts"
  }
  ...
});
</code></pre>

<p>Now, we can use the <code>$t</code> function in our components &mdash; as shown below &mdash; to parse strings from our internationalization configurations.</p>

<p><strong>Note</strong>: <em>There’s no need to import <code>$t</code> since we have Nuxt’s default auto-import functionality.</em></p>

<pre><code class="language-html">&lt;!-- i18n.config.ts --&gt;
&lt;template&gt;
  &lt;div&gt;
    &lt;v-card color="cardBackground"&gt;
      &lt;v-card-title class="text-overline"&gt;
        {{ $t("homePage.title") }}
      &lt;/v-card-title&gt;
      &lt;v-card-text&gt;
        {{ $t("homePage.description") }}
      &lt;/v-card-text&gt;
    &lt;/v-card&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/1-home-page.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="316"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/1-home-page.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/1-home-page.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/1-home-page.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/1-home-page.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/1-home-page.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/1-home-page.png"
			
			sizes="100vw"
			alt="Page with a title and description"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/1-home-page.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="lazy-loading-translations">Lazy Loading Translations</h2>

<p>We have the title and description served from the configurations. Next, we can add more languages to the same config. For example, here’s how we can establish translations for English (<code>en</code>), French (<code>fr</code>) and Spanish (<code>es</code>):</p>

<pre><code class="language-javascript">// i18n.config.ts
export default defineI18nConfig(() =&gt; ({
  legacy: false,
  locale: "en",
  messages: {
    en: {
      // English
    },
    fr: {
      // French
    },
    es: {
      // Spanish
    }
  },
}));
</code></pre>

<p>For a production website with a lot of content that needs translating, it would be unwise to bundle all of the messages from different locales in the main bundle. Instead, we should use the <code>nuxt-i18</code> lazy loading feature asynchronously load only the required language rather than all of them at once. Also, having messages for all locales in a single configuration file can become difficult to manage over time, and breaking them up like this makes things easier to find.</p>

<p>Let’s set up the lazy loading feature in <code>nuxt.config.ts</code>:</p>

<pre><code class="language-javascript">// etc.
  i18n: {
    vueI18n: "./i18n.config.ts",
    lazy: true,
    langDir: "locales",
    locales: [
      {
        code: "en",
        file: "en.json",
        name: "English",
      },
      {
        code: "es",
        file: "es.json",
        name: "Spanish",
      },
      {
        code: "fr",
        file: "fr.json",
        name: "French",
      },
    ],
    defaultLocale: "en",
    strategy: "no_prefix",
  },

// etc.
</code></pre>

<p>This enables lazy loading and specifies the <code>locales</code> directory that will contain our locale files. The <code>locales</code> array configuration specifies from which files Nuxt.js should pick up messages for a specific language.</p>

<p>Now, we can create individual files for each language. I’ll drop all three of them right here:</p>

<pre><code class="language-javascript">
// locales/en.json
{
  "homePage": {
    "title": "Home",
    "description": "This is the home page description."
  },
  "aboutPage": {
    "title": "About",
    "description": "This is the about page description."
  },
  "selectLocale": {
    "label": "Select Locale"
  },
  "navbar": {
    "homeButton": "Home",
    "aboutButton": "About"
  }
}
</code></pre>

<div class="break-out">
<pre><code class="language-javascript">// locales/fr.json
{
  "homePage": {
    "title": "Bienvenue sur la page d'accueil",
    "description": "Ceci est la description de la page d'accueil."
  },
  "aboutPage": {
    "title": "À propos de nous",
    "description": "Ceci est la description de la page à propos de nous."
  },
  "selectLocale": {
    "label": "Sélectionner la langue"
  },
  "navbar": {
    "homeButton": "Accueil",
    "aboutButton": "À propos"
  }
}
</code></pre>
</div>

<div class="break-out">
<pre><code class="language-javascript">// locales/es.json
{
  "homePage": {
    "title": "Bienvenido a la página de inicio",
    "description": "Esta es la descripción de la página de inicio."
  },
  "aboutPage": {
    "title": "Sobre nosotros",
    "description": "Esta es la descripción de la página sobre nosotros."
  },
  "selectLocale": {
    "label": "Seleccione el idioma"
  },
  "navbar": {
    "homeButton": "Inicio",
    "aboutButton": "Acerca de"
  }
}
</code></pre>
</div>

<p>We have set up lazy loading, added multiple languages to our application, and moved our locale messages to separate files. The user gets the right locale for the right message, and the locale messages are kept in a maintainable manner inside the code base.</p>

<h2 id="switching-between-languages">Switching Between Languages</h2>

<p>We have different locales, but to see them in action, we will build a component that can be used to switch between the available locales.</p>

<pre><code class="language-html">&lt;!-- components/select-locale.vue --&gt;
&lt;script setup&gt;
const { locale, locales, setLocale } = useI18n();

const language = computed({
  get: () =&gt; locale.value,
  set: (value) =&gt; setLocale(value),
});
&lt;/script&gt;

&lt;template&gt;
  &lt;v-select
    :label="$t('selectLocale.label')"
    variant="outlined"
    color="primary"
    density="compact"    
    :items="locales"
    item-title="name"
    item-value="code"
    v-model="language"
  &gt;&lt;/v-select&gt;
&lt;/template&gt;
</code></pre>

<p>This component uses the <code>useI18n</code> hook provided by the Vue I18n library and a computed property <code>language</code> to get and set the global locale from a <code>&lt;select&gt;</code> input. To make this even more like a real-world website, we’ll include a small navigation bar that links up all of the website’s pages.</p>

<pre><code class="language-html">&lt;!-- components/select-locale.vue --&gt;
&lt;template&gt;
  &lt;v-app-bar app :elevation="2" class="px-2"&gt;
    &lt;div&gt;
      &lt;v-btn color="button" to="/"&gt;
        {{ $t("navbar.homeButton") }}
      &lt;/v-btn&gt;
      &lt;v-btn color="button" to="/about"&gt;
        {{ $t("navbar.aboutButton") }}
      &lt;/v-btn&gt;
    &lt;/div&gt;
    &lt;v-spacer /&gt;
    &lt;div class="mr-4 mt-6"&gt;
      &lt;SelectLocale /&gt;
    &lt;/div&gt;
  &lt;/v-app-bar&gt;
&lt;/template&gt;
</code></pre>

<p>That’s it! Now, we can switch between languages on the fly.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/2-current-state-homepage.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="338"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/2-current-state-homepage.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/2-current-state-homepage.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/2-current-state-homepage.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/2-current-state-homepage.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/2-current-state-homepage.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/2-current-state-homepage.png"
			
			sizes="100vw"
			alt="Showing the current state of the homepage."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Showing the current state of the homepage. (<a href='https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/2-current-state-homepage.png'>Large preview</a>)
    </figcaption>
  
</figure>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/3-homepage-french-translation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="362"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/3-homepage-french-translation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/3-homepage-french-translation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/3-homepage-french-translation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/3-homepage-french-translation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/3-homepage-french-translation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/3-homepage-french-translation.png"
			
			sizes="100vw"
			alt="Homepage showing a French translation."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Showing a French translation. (<a href='https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/3-homepage-french-translation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>We have a basic layout, but I thought we’d take this a step further and build a playground page we can use to explore more i18n features that are pretty useful when building a multilingual website.</p>

<h2 id="interpolation-and-pluralization">Interpolation and Pluralization</h2>

<p>Interpolation and pluralization are internationalization techniques for handling dynamic content and grammatical variations across different languages. <strong>Interpolation</strong> allows developers to insert dynamic variables or expressions into translated strings. <strong>Pluralization</strong> addresses the complexities of plural forms in languages by selecting the appropriate grammatical form based on numeric values. With the help of interpolation and pluralization, we can create more natural and accurate translations.</p>

<p>To use pluralization in our Nuxt app, we’ll first add a configuration to the English locale file.</p>

<pre><code class="language-javascript">// locales/en.json
{
  // etc.
  "playgroundPage": {
    "pluralization": {
      "title": "Pluralization",
      "apple": "No Apple | One Apple | {count} Apples",
      "addApple": "Add"
    }
  }
  // etc.
}
</code></pre>

<p>The pluralization configuration set up for the key <code>apple</code> defines an output &mdash; <code>No Apple</code> &mdash; if a count of 0 is passed to it, a second output &mdash; <code>One Apple</code> &mdash; if a count of 1 is passed, and a third &mdash; <code>2 Apples</code>, <code>3 Apples</code>, and so on &mdash; if the count passed in is greater than 1.</p>

<p>Here is how we can use it in your component: Whenever you click on the add button, you will see pluralization in action, changing the strings.</p>

<div class="break-out">
<pre><code class="language-html">&lt;!-- pages/playground.vue --&gt;
&lt;script setup&gt;
let appleCount = ref(0);
const addApple = () =&gt; {
  appleCount.value += 1;
};
&lt;/script&gt;
&lt;template&gt;
  &lt;v-container fluid&gt;
    &lt;!-- PLURALIZATION EXAMPLE  --&gt;
    &lt;v-card color="cardBackground"&gt;
      &lt;v-card-title class="text-overline"&gt;
        {{ $t("playgroundPage.pluralization.title") }}
      &lt;/v-card-title&gt;

      &lt;v-card-text&gt;
        {{ $t("playgroundPage.pluralization.apple", { count: appleCount }) }}
      &lt;/v-card-text&gt;
      &lt;v-card-actions&gt;
        &lt;v-btn
          @click="addApple"
          color="primary"
          variant="outlined"
          density="comfortable"
          &gt;{{ $t("playgroundPage.pluralization.addApple") }}&lt;/v-btn
        &gt;
      &lt;/v-card-actions&gt;
    &lt;/v-card&gt;
  &lt;/v-container&gt;
&lt;/template&gt;
</code></pre>
</div>

<p>To use interpolation in our Nuxt app, first, add a configuration in the English locale file:</p>

<div class="break-out">
<pre><code class="language-javascript">// locales/en.json
{
  ...
  "playgroundPage": {
    ... 
    "interpolation": {
      "title": "Interpolation",
      "sayHello": "Hello, {name}",
      "hobby": "My favourite hobby is {0}.",
      "email": "You can reach out to me at {account}{'@'}{domain}.com"
    },
    // etc. 
  }
  // etc.
}
</code></pre>
</div>

<p>The message for <code>sayHello</code> expects an object passed to it having a key <code>name</code> when invoked &mdash; a process known as <a href="https://vue-i18n.intlify.dev/guide/essentials/syntax#named-interpolation">named interpolation</a>.</p>

<p>The message <code>hobby</code> expects an array to be passed to it and will pick up the 0<sup>th</sup> element, which is known as <a href="https://vue-i18n.intlify.dev/guide/essentials/syntax#list-interpolation">list interpolation</a>.</p>

<p>The message <code>email</code> expects an object with keys <code>account</code>, and <code>domain</code> and joins both with a literal string <code>&quot;@&quot;</code>. This is known as <a href="https://vue-i18n.intlify.dev/guide/essentials/syntax#literal-interpolation">literal interpolation</a>.</p>

<p>Below is an example of how to use it in the Vue components:</p>

<div class="break-out">
<pre><code class="language-html">&lt;!-- pages/playground.vue --&gt;
&lt;template&gt;
  &lt;v-container fluid&gt;
    &lt;!-- INTERPOLATION EXAMPLE  --&gt;
    &lt;v-card color="cardBackground"&gt;
      &lt;v-card-title class="text-overline"&gt;
        {{ $t("playgroundPage.interpolation.title") }}
      &lt;/v-card-title&gt;
      &lt;v-card-text&gt;
        &lt;p&gt;
          {{
            $t("playgroundPage.interpolation.sayHello", {
              name: "Jane",
            })
          }}
        &lt;/p&gt;
        &lt;p&gt;
          {{
            $t("playgroundPage.interpolation.hobby", ["Football", "Cricket"])
          }}
        &lt;/p&gt;
        &lt;p&gt;
          {{
            $t("playgroundPage.interpolation.email", {
              account: "johndoe",
              domain: "hygraph",
            })
          }}
        &lt;/p&gt;
      &lt;/v-card-text&gt;
    &lt;/v-card&gt;
  &lt;/v-container&gt;
&lt;/template&gt;
</code></pre>
</div>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/4-pluralization-interpolation.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="501"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/4-pluralization-interpolation.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/4-pluralization-interpolation.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/4-pluralization-interpolation.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/4-pluralization-interpolation.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/4-pluralization-interpolation.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/4-pluralization-interpolation.png"
			
			sizes="100vw"
			alt="Exampe of pluralization and interpolation"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/4-pluralization-interpolation.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="date-time-translations">Date &amp; Time Translations</h2>

<p>Translating dates and times involves translating date and time formats according to the conventions of different locales. We can use Vue I18n’s features for formatting date strings, handling time zones, and translating day and month names for managing date time translations. We can give the configuration for the same using the <code>datetimeFormats</code> key inside the <code>vue-i18n</code> config object.</p>

<pre><code class="language-javascript">// i18n.config.ts
export default defineI18nConfig(() =&gt; ({
  fallbackLocale: "en",
  datetimeFormats: {
    en: {
      short: {
        year: "numeric",
        month: "short",
        day: "numeric",
      },
      long: {
        year: "numeric",
        month: "short",
        day: "numeric",
        weekday: "short",
        hour: "numeric",
        minute: "numeric",
        hour12: false,
      },
    },
    fr: {
      short: {
        year: "numeric",
        month: "short",
        day: "numeric",
      },
      long: {
        year: "numeric",
        month: "short",
        day: "numeric",
        weekday: "long",
        hour: "numeric",
        minute: "numeric",
        hour12: true,
      },
    },
    es: {
      short: {
        year: "numeric",
        month: "short",
        day: "numeric",
      },
      long: {
        year: "2-digit",
        month: "short",
        day: "numeric",
        weekday: "long",
        hour: "numeric",
        minute: "numeric",
        hour12: true,
      },
    },
  },
}));
</code></pre>

<p>Here, we have set up <code>short</code> and <code>long</code> formats for all three languages. If you are coding along, you will be able to see available configurations for fields, like month and year, thanks to TypeScript and Intellisense features provided by your code editor. To display the translated dates and times in components, we should use the <code>$d</code> function and pass the format to it.</p>

<div class="break-out">
<pre><code class="language-html">&lt;!-- pages.playground.vue --&gt;
&lt;template&gt;
  &lt;v-container fluid&gt;
    &lt;!-- DATE TIME TRANSLATIONS EXAMPLE  --&gt;
    &lt;v-card color="cardBackground"&gt;
      &lt;v-card-title class="text-overline"&gt;
        {{ $t("playgroundPage.dateTime.title") }}
      &lt;/v-card-title&gt;
      &lt;v-card-text&gt;
        &lt;p&gt;Short: {{ (new Date(), $d(new Date(), "short")) }}&lt;/p&gt;
        &lt;p&gt;Long: {{ (new Date(), $d(new Date(), "long")) }}&lt;/p&gt;
      &lt;/v-card-text&gt;
    &lt;/v-card&gt;
  &lt;/v-container&gt;
&lt;/template&gt;
</code></pre>
</div>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/5-default-date-time.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="337"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/5-default-date-time.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/5-default-date-time.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/5-default-date-time.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/5-default-date-time.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/5-default-date-time.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/5-default-date-time.png"
			
			sizes="100vw"
			alt="Showing a default date and time."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Showing a default date and time. (<a href='https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/5-default-date-time.png'>Large preview</a>)
    </figcaption>
  
</figure>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/6-date-time-translated-spanish.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="338"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/6-date-time-translated-spanish.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/6-date-time-translated-spanish.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/6-date-time-translated-spanish.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/6-date-time-translated-spanish.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/6-date-time-translated-spanish.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/6-date-time-translated-spanish.png"
			
			sizes="100vw"
			alt="Showing the date and time translated in Spanish."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Showing the date and time translated in Spanish. (<a href='https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/6-date-time-translated-spanish.png'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="localization-on-the-hygraph-side">Localization On the Hygraph Side</h2>

<p>We saw how to implement localization with static content. Now, we’ll attempt to understand how to fetch dynamic localized content in Nuxt.</p>

<p>We can build a blog page in our Nuxt App that fetches data from a server. The server API should accept a locale and return data in that specific locale.</p>

<p>Hygraph has a flexible localization API that allows you to publish and query localized content. If you haven’t created a free Hygraph account yet, <a href="https://hygraph.com/">you can do that on the Hygraph website</a> to continue following along.</p>

<p>Go to <strong>Project Settings</strong> &rarr; <strong>Locales</strong> and add locales for the API.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/7-hygraph-studio-locales.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="409"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/7-hygraph-studio-locales.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/7-hygraph-studio-locales.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/7-hygraph-studio-locales.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/7-hygraph-studio-locales.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/7-hygraph-studio-locales.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/7-hygraph-studio-locales.png"
			
			sizes="100vw"
			alt="Showing Locales on the Hygraph website"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/7-hygraph-studio-locales.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>We have added two locales: English and French. Now we need aq <code>localized_post</code> model in our schema that only two fields: <code>title</code> and <code>body.</code> Ensure to make these “Localized” fields while creating them.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/8-hygraph-studio-schema.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="331"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/8-hygraph-studio-schema.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/8-hygraph-studio-schema.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/8-hygraph-studio-schema.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/8-hygraph-studio-schema.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/8-hygraph-studio-schema.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/8-hygraph-studio-schema.png"
			
			sizes="100vw"
			alt="Showing Schema, which has title and body fields, on the Htygraph website"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/8-hygraph-studio-schema.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Add permissions to consume the localized content, go to <strong>Project settings</strong> &rarr; <strong>Access</strong> &rarr; <strong>API Access</strong> &rarr; <strong>Public Content API</strong>, and assign <strong>Read</strong> permissions to the <code>localized_post</code> model.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/9-hygraph-studio-permissions.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="394"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/9-hygraph-studio-permissions.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/9-hygraph-studio-permissions.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/9-hygraph-studio-permissions.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/9-hygraph-studio-permissions.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/9-hygraph-studio-permissions.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/9-hygraph-studio-permissions.png"
			
			sizes="100vw"
			alt="Project settings on the Htygraph website with Public Content API and an assigned Read permission"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/how-build-multilingual-website-nuxtjs/9-hygraph-studio-permissions.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Now, we can go to the <a href="https://hygraph.com/docs/api-reference/basics/api-playground">Hygrapgh API playground</a> and add some localized data to the database with the help of GraphQL mutations. To limit the scope of this example, I am simply adding data from the Hygraph API playground. In an ideal world, a create/update mutation would be triggered from the front end after receiving user input.</p>

<p>Run this mutation in the Hygraph API playground:</p>

<div class="break-out">
<pre><code class="language-javascript">mutation createLocalizedPost {
  createLocalizedPost(
    data: {
      title: "A Journey Through the Alps", 
      body: "Exploring the majestic mountains of the Alps offers a thrilling experience. The stunning landscapes, diverse wildlife, and pristine environment make it a perfect destination for nature lovers.", 
      localizations: {
        create: [
          {locale: fr, data: {title: "Un voyage à travers les Alpes", body: "Explorer les majestueuses montagnes des Alpes offre une expérience palpitante. Les paysages époustouflants, la faune diversifiée et l'environnement immaculé en font une destination parfaite pour les amoureux de la nature."}}
        ]
      }
    }
  ) {
    id
  }
}
</code></pre>
</div>

<p>The mutation above creates a post with the <code>en</code> locale and includes a <code>fr</code> version of the same post. Feel free to add more data to your model if you want to see things work from a broader set of data.</p>

<h2 id="putting-things-together">Putting Things Together</h2>

<p>Now that we have Hygraph API content ready for consumption let’s take a moment to understand how it’s consumed inside the Nuxt app.</p>

<p>To do this, we’ll install <a href="https://nuxt.com/modules/graphql-client">nuxt-graphql-client</a> to serve as the app’s GraphQL client. This is a minimal GraphQL client for performing GraphQL operations without having to worry about complex configurations, code generation, typing, and other setup tasks.</p>

<pre><code class="language-bash">npx nuxi@latest module add graphql-client
</code></pre>

<pre><code class="language-javascript">// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    // ...
    "nuxt-graphql-client"
    // ...
  ],
  runtimeConfig: {
    public: {
      GQL_HOST: 'ADD&#95;YOUR&#95;GQL&#95;HOST&#95;URL&#95;HERE&#95;OR&#95;IN&#95;.env'
    }
  },
});
</code></pre>

<p>Next, let&rsquo;s add our GraphQL queries in <code>graphql/queries.graphql</code>.</p>

<pre><code class="language-javascript">query getPosts($locale: [Locale!]!) {
  localizedPosts(locales: $locale) {
    title
    body
  }
}
</code></pre>

<p>The GraphQL client will automatically scan <code>.graphql</code> and <code>.gql</code> files and generate client-side code and typings in the <code>.nuxt/gql</code> folder. All we need to do is stop and restart the Nuxt application. After restarting the app, the GraphQL client will allow us to use a <code>GqlGetPosts</code> function to trigger the query.</p>

<p>Now, we will build the Blog page where by querying the Hygraph server and showing the dynamic data.</p>

<pre><code class="language-javascript">// pages/blog.vue
&lt;script lang="ts" setup&gt;
  import type { GetPostsQueryVariables } from "#gql";
  import type { PostItem, Locale } from "../types/types";

  const { locale } = useI18n();
  const posts = ref&lt;PostItem[]&gt;([]);
  const isLoading = ref(false);
  const isError = ref(false);

  const fetchPosts = async (localeValue: Locale) =&gt; {
    try {
      isLoading.value = true;
      const variables: GetPostsQueryVariables = {
        locale: [localeValue],
      };
      const data = await GqlGetPosts(variables);
      posts.value = data?.localizedPosts ?? [];
    } catch (err) {
      console.log("Fetch Error, Something went wrong", err);
      isError.value = true;
    } finally {
      isLoading.value = false;
    }
  };

  // Fetch posts on component mount
  onMounted(() =&gt; {
    fetchPosts(locale.value as Locale);
  });

  // Watch for locale changes
  watch(locale, (newLocale) =&gt; {
    fetchPosts(newLocale as Locale);
  });
&lt;/script&gt;
</code></pre>

<p>This code fetches only the current locale from the <code>useI18n</code> hook and sends it to the <code>fetchPosts</code> function when the Vue component is mounted. The <code>fetchPosts</code> function will pass the locale to the GraphQL query as a variable and obtain localized data from the Hygraph server. We also have a watcher on the <code>locale</code> so that whenever the global locale is changed by the user we make an API call to the server again and fetch posts in that locale.</p>

<p>And, finally, let’s add markup for viewing our fetched data!</p>

<div class="break-out">
<pre><code class="language-html">&lt;!-- pages/blog.vue --&gt;
&lt;template&gt;
  &lt;v-container fluid&gt;
    &lt;v-card-title class="text-overline"&gt;Blogs&lt;/v-card-title&gt;
    &lt;div v-if="isLoading"&gt;
      &lt;v-skeleton-loader type="card" v-for="n in 2" :key="n" class="mb-4" /&gt;
    &lt;/div&gt;
    &lt;div v-else-if="isError"&gt;
      &lt;p&gt;Something went wrong while getting blogs please check the logs.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div v-else&gt;
      &lt;div
        v-for="(post, index) in posts"
        :key="post.title || index"
        class="mb-4"
      &gt;
        &lt;v-card color="cardBackground"&gt;
          &lt;v-card-title class="text-h6"&gt;{{ post.title }}&lt;/v-card-title&gt;
          &lt;v-card-text&gt;{{ post.body }}&lt;/v-card-text&gt;
        &lt;/v-card&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/v-container&gt;
&lt;/template&gt;
</code></pre>
</div>

<p>Awesome! If all goes according to plan, then your app should look something like the one in the following video.</p>


<figure class="video-embed-container break-out">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/992462029"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>Check that out &mdash; we just made the <strong>functionality for translating content for a multilingual website</strong>! Now, a user can select a locale from a list of options, and the app fetches content for the selected locale and automatically updates the displayed content.</p>

<p>Did you think that translations would require more difficult steps? It’s pretty amazing that we’re able to cobble together a couple of libraries, hook them up to an API, and wire everything up to render on a page.</p>

<p>Of course, there are other libraries and resources for handling internationalization in a multilingual context. The exact tooling is less the point than it is seeing what pieces are needed to handle dynamic translations and how they come together.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Juan Diego Rodríguez</author><title>Uniting Web And Native Apps With 4 Unknown JavaScript APIs</title><link>https://www.smashingmagazine.com/2024/06/uniting-web-native-apps-unknown-javascript-apis/</link><pubDate>Thu, 20 Jun 2024 18:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/06/uniting-web-native-apps-unknown-javascript-apis/</guid><description>Have you heard of the Screen Orientation API? What about the Device Orientation API, Vibration API, or the Contact Picker API? Juan Diego Rodriguez is interested in these under-the-radar web features and discusses how they can be used to create more usable and robust progressive web apps if and when they gain broader support.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/06/uniting-web-native-apps-unknown-javascript-apis/" />
              <title>Uniting Web And Native Apps With 4 Unknown JavaScript APIs</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Uniting Web And Native Apps With 4 Unknown JavaScript APIs</h1>
                  
                    
                    <address>Juan Diego Rodríguez</address>
                  
                  <time datetime="2024-06-20T18:00:00&#43;00:00" class="op-published">2024-06-20T18:00:00+00:00</time>
                  <time datetime="2024-06-20T18:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>A couple of years ago, <a href="https://www.smashingmagazine.com/2022/09/javascript-api-guide/">four JavaScript APIs that landed at the bottom of awareness in the State of JavaScript survey</a>. I took an interest in those APIs because they have so much potential to be useful but don’t get the credit they deserve. Even after a quick search, I was amazed at how many new web APIs have been added to the ECMAScript specification that aren’t getting their dues and with a lack of awareness and browser support in browsers.</p>

<p>That situation can be a “catch-22”:</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aAn%20API%20is%20interesting%20but%20lacks%20awareness%20due%20to%20incomplete%20support,%20and%20there%20is%20no%20immediate%20need%20to%20support%20it%20due%20to%20low%20awareness.%0a&url=https://smashingmagazine.com%2f2024%2f06%2funiting-web-native-apps-unknown-javascript-apis%2f">
      
An API is interesting but lacks awareness due to incomplete support, and there is no immediate need to support it due to low awareness.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>Most of these APIs are designed to power progressive web apps (PWA) and close the gap between web and native apps. Bear in mind that creating a PWA involves more than just adding a <a href="https://css-tricks.com/how-to-transition-to-manifest-v3-for-chrome-extensions/">manifest file</a>. Sure, it’s a PWA by definition, but it functions like a bookmark on your home screen in practice. In reality, we need several APIs to achieve a fully native app experience on the web. And the four APIs I’d like to shed light on are part of that PWA puzzle that brings to the web what we once thought was only possible in native apps.</p>

<p>You can see all these <a href="https://monknow.github.io/pwa-features-demo/">APIs in action in this demo</a> as we go along.</p>

<h2 id="1-screen-orientation-api">1. Screen Orientation API</h2>

<p>The <a href="https://www.w3.org/TR/screen-orientation/">Screen Orientation API</a> can be used to sniff out the device’s current orientation. Once we know whether a user is browsing in a portrait or landscape orientation, we can use it to <strong>enhance the UX for mobile devices</strong> by changing the UI accordingly. We can also use it to <strong>lock the screen in a certain position</strong>, which is useful for displaying videos and other full-screen elements that benefit from a wider viewport.</p>

<p>Using the global <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/screen"><code>screen</code></a> object, you can access various properties the screen uses to render a page, including the <code>screen.orientation</code> object. It has two properties:</p>

<ul>
<li><strong><code>type</code>:</strong> The current screen orientation. It can be: <code>&quot;portrait-primary&quot;</code>, <code>&quot;portrait-secondary&quot;</code>, <code>&quot;landscape-primary&quot;</code>, or <code>&quot;landscape-secondary&quot;</code>.</li>
<li><strong><code>angle</code>:</strong> The current screen orientation angle. It can be any number from 0 to 360 degrees, but it’s normally set in multiples of 90 degrees (e.g., <code>0</code>, <code>90</code>, <code>180</code>, or <code>270</code>).</li>
</ul>

<p>On mobile devices, if the <code>angle</code> is <code>0</code> degrees, the <code>type</code> is most often going to evaluate to <code>&quot;portrait&quot;</code> (vertical), but on desktop devices, it is typically <code>&quot;landscape&quot;</code> (horizontal). This makes the <code>type</code> property precise for knowing a device’s true position.</p>

<p>The <code>screen.orientation</code> object also has two methods:</p>

<ul>
<li><strong><code>.lock()</code>:</strong> This is an async method that takes a <code>type</code> value as an argument to lock the screen.</li>
<li><strong><code>.unlock()</code>:</strong> This method unlocks the screen to its default orientation.</li>
</ul>

<p>And lastly, <code>screen.orientation</code> counts with an <code>&quot;orientationchange&quot;</code> event to know when the orientation has changed.</p>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p>Roll up your sleeves and <strong>boost your UX skills</strong>! Meet <strong><a data-instant href="https://smart-interface-design-patterns.com/">Smart Interface Design Patterns</a></strong>&nbsp;🍣, a 10h video library by Vitaly Friedman. <strong>100s of real-life examples</strong> and live UX training. <a href="https://www.youtube.com/watch?v=3mwZztmGgbE">Free preview</a>.</p>
<a data-instant href="https://smart-interface-design-patterns.com/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://smart-interface-design-patterns.com/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3155f571-450d-42f9-81b4-494aa9b52841/video-course-smart-interface-design-patterns-vitaly-friedman.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8c98e7f9-8e62-4c43-b833-fc6bf9fea0a9/video-course-smart-interface-design-patterns-vitaly-friedman.jpg"
    alt="Feature Panel"
    width="690"
    height="790"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h3 id="browser-support">Browser Support</h3>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://caniuse.com/screen-orientation">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="289"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg"
			
			sizes="100vw"
			alt="Browser Support on Screen Orientation API"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Source: <a href='https://caniuse.com/screen-orientation'>Caniuse</a>. (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-api-support.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="finding-and-locking-screen-orientation">Finding And Locking Screen Orientation</h3>

<p>Let’s code a short demo using the Screen Orientation API to know the device’s orientation and lock it in its current position.</p>

<p>This can be our HTML boilerplate:</p>

<div class="break-out">
<pre><code class="language-html">&lt;main&gt;
  &lt;p&gt;
    Orientation Type: &lt;span class="orientation-type"&gt;&lt;/span&gt;
    &lt;br /&gt;
    Orientation Angle: &lt;span class="orientation-angle"&gt;&lt;/span&gt;
  &lt;/p&gt;

  &lt;button type="button" class="lock-button"&gt;Lock Screen&lt;/button&gt;

  &lt;button type="button" class="unlock-button"&gt;Unlock Screen&lt;/button&gt;

  &lt;button type="button" class="fullscreen-button"&gt;Go Full Screen&lt;/button&gt;
&lt;/main&gt;
</code></pre>
</div>

<p>On the JavaScript side, we inject the screen orientation <code>type</code> and <code>angle</code> properties into our HTML.</p>

<div class="break-out">
<pre><code class="language-javascript">let currentOrientationType = document.querySelector(".orientation-type");
let currentOrientationAngle = document.querySelector(".orientation-angle");

currentOrientationType.textContent = screen.orientation.type;
currentOrientationAngle.textContent = screen.orientation.angle;
</code></pre>
</div>

<p>Now, we can see the device’s orientation and angle properties. On my laptop, they are <code>&quot;landscape-primary&quot;</code> and <code>0°</code>.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="367"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png"
			
			sizes="100vw"
			alt="Screen Orientation type and angle being displayed"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-1.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>If we listen to the window’s <code>orientationchange</code> event, we can see how the values are updated each time the screen rotates.</p>

<pre><code class="language-javascript">window.addEventListener("orientationchange", () =&gt; {
  currentOrientationType.textContent = screen.orientation.type;
  currentOrientationAngle.textContent = screen.orientation.angle;
});
</code></pre>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="679"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png"
			
			sizes="100vw"
			alt="Screen Orientation type and angle are displayed in portrait mode"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/screen-orientation-2.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To lock the screen, we need to first be in full-screen mode, so we will use another extremely useful feature: the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API"><strong>Fullscreen API</strong></a>. Nobody wants a webpage to pop into full-screen mode without their consent, so we need <a href="https://developer.mozilla.org/en-US/docs/Web/Security/User_activation#transient_activation">transient activation</a> (i.e., a user click) from a DOM element to work.</p>

<p>The Fullscreen API has two methods:</p>

<ol>
<li><code>Document.exitFullscreen()</code> is used from the global document object,</li>
<li><code>Element.requestFullscreen()</code> makes the specified element and its descendants go full-screen.</li>
</ol>

<p>We want the entire page to be full-screen so we can invoke the method from the root element at the <code>document.documentElement</code> object:</p>

<div class="break-out">
<pre><code class="language-javascript">const fullscreenButton = document.querySelector(".fullscreen-button");

fullscreenButton.addEventListener("click", async () =&gt; {
  // If it is already in full-screen, exit to normal view
  if (document.fullscreenElement) {
    await document.exitFullscreen();
  } else {
    await document.documentElement.requestFullscreen();
  }
});
</code></pre>
</div>

<p>Next, we can lock the screen in its current orientation:</p>

<pre><code class="language-javascript">const lockButton = document.querySelector(".lock-button");

lockButton.addEventListener("click", async () =&gt; {
  try {
    await screen.orientation.lock(screen.orientation.type);
  } catch (error) {
    console.error(error);
  }
});
</code></pre>

<p>And do the opposite with the unlock button:</p>

<pre><code class="language-javascript">const unlockButton = document.querySelector(".unlock-button");

unlockButton.addEventListener("click", () =&gt; {
  screen.orientation.unlock();
});
</code></pre>

<h3 id="can-t-we-check-orientation-with-a-media-query">Can’t We Check Orientation With a Media Query?</h3>

<p>Yes! We can indeed check page orientation via the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/orientation"><code>orientation</code> media feature</a> in a CSS media query. However, media queries compute the current orientation by checking if the width is “bigger than the height” for landscape or “smaller” for portrait. By contrast,</p>

<blockquote class="pull-quote">
  <p>
    <a class="pull-quote__link" aria-label="Share on Twitter" href="https://twitter.com/share?text=%0aThe%20Screen%20Orientation%20API%20checks%20for%20the%20screen%20rendering%20the%20page%20regardless%20of%20the%20viewport%20dimensions,%20making%20it%20resistant%20to%20inconsistencies%20that%20may%20crop%20up%20with%20page%20resizing.%0a&url=https://smashingmagazine.com%2f2024%2f06%2funiting-web-native-apps-unknown-javascript-apis%2f">
      
The Screen Orientation API checks for the screen rendering the page regardless of the viewport dimensions, making it resistant to inconsistencies that may crop up with page resizing.

    </a>
  </p>
  <div class="pull-quote__quotation">
    <div class="pull-quote__bg">
      <span class="pull-quote__symbol">“</span></div>
  </div>
</blockquote>

<p>You may have noticed how PWAs like Instagram and X force the screen to be in portrait mode even when the native system orientation is unlocked. It is important to notice that this behavior isn’t achieved through the Screen Orientation API, but by setting the <code>orientation</code> property on the <code>manifest.json</code> file to the desired orientation type.</p>

<h2 id="2-device-orientation-api">2. Device Orientation API</h2>

<p>Another API I’d like to poke at is the Device Orientation API. It provides access to a device’s gyroscope sensors to read the device’s orientation in space; something used all the time in mobile apps, mainly games. The API makes this happen with a <code>deviceorientation</code> event that triggers each time the device moves. It has the following properties:</p>

<ul>
<li><strong><code>event.alpha</code>:</strong> Orientation along the Z-axis, ranging from 0 to 360 degrees.</li>
<li><strong><code>event.beta</code>:</strong> Orientation along the X-axis, ranging from -180 to 180 degrees.</li>
<li><strong><code>event.gamma</code>:</strong> Orientation along the Y-axis, ranging from -90 to 90 degrees.</li>
</ul>

<h3 id="browser-support-1">Browser Support</h3>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://caniuse.com/deviceorientation">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="293"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg"
			
			sizes="100vw"
			alt="Browser Support on Device Orientation API"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Source: <a href='https://caniuse.com/deviceorientation'>Caniuse</a>. (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-orientation-api-support.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="moving-elements-with-your-device">Moving Elements With Your Device</h3>

<p>In this case, we will make a 3D cube with CSS that can be rotated with your device! The full instructions I used to make the initial CSS cube are credited to <a href="https://desandro.com">David DeSandro</a> and can be <a href="https://3dtransforms.desandro.com/cube">found in his introduction to 3D transforms</a>.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="vYwdMNJ"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Rotate cube [forked]](https://codepen.io/smashingmag/pen/vYwdMNJ) by <a href="https://codepen.io/desandro">Dave DeSandro</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/vYwdMNJ">Rotate cube [forked]</a> by <a href="https://codepen.io/desandro">Dave DeSandro</a>.</figcaption>
</figure>

<p>You can see raw full HTML in the demo, but let’s print it here for posterity:</p>

<pre><code class="language-html">&lt;main&gt;
  &lt;div class="scene"&gt;
    &lt;div class="cube"&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--front"&gt;1&lt;/div&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--back"&gt;2&lt;/div&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--right"&gt;3&lt;/div&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--left"&gt;4&lt;/div&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--top"&gt;5&lt;/div&gt;
      &lt;div class="cube&#95;&#95;face cube&#95;&#95;face--bottom"&gt;6&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;h1&gt;Device Orientation API&lt;/h1&gt;
  &lt;p&gt;
    Alpha: &lt;span class="currentAlpha"&gt;&lt;/span&gt;
    &lt;br /&gt;
    Beta: &lt;span class="currentBeta"&gt;&lt;/span&gt;
    &lt;br /&gt;
    Gamma: &lt;span class="currentGamma"&gt;&lt;/span&gt;
  &lt;/p&gt;
&lt;/main&gt;
</code></pre>

<p>To keep this brief, I won’t explain the CSS code here. Just keep in mind that it provides the necessary styles for the 3D cube, and it can be rotated through all axes using the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotate">CSS <code>rotate()</code> function</a>.</p>

<p>Now, with JavaScript, we listen to the window’s <code>deviceorientation</code> event and access the event orientation data:</p>

<pre><code class="language-javascript">const currentAlpha = document.querySelector(".currentAlpha");
const currentBeta = document.querySelector(".currentBeta");
const currentGamma = document.querySelector(".currentGamma");

window.addEventListener("deviceorientation", (event) =&gt; {
  currentAlpha.textContent = event.alpha;
  currentBeta.textContent = event.beta;
  currentGamma.textContent = event.gamma;
});
</code></pre>

<p>To see how the data changes on a desktop device, we can open Chrome’s DevTools and access the Sensors Panel to emulate a rotating device.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="601"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png"
			
			sizes="100vw"
			alt="Emulating a device rotating on the Chrome DevTools Sensor Panel"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-1.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>To rotate the cube, we change its CSS <code>transform</code> properties according to the device orientation data:</p>

<div class="break-out">
<pre><code class="language-javascript">const currentAlpha = document.querySelector(".currentAlpha");
const currentBeta = document.querySelector(".currentBeta");
const currentGamma = document.querySelector(".currentGamma");

const cube = document.querySelector(".cube");

window.addEventListener("deviceorientation", (event) =&gt; {
  currentAlpha.textContent = event.alpha;
  currentBeta.textContent = event.beta;
  currentGamma.textContent = event.gamma;

  cube.style.transform = `rotateX(${event.beta}deg) rotateY(${event.gamma}deg) rotateZ(${event.alpha}deg)`;
});
</code></pre>
</div>

<p>This is the result:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="392"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg"
			
			sizes="100vw"
			alt="Cube rotated according to emulated device orientation"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/device-motion-2.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h2 id="3-vibration-api">3. Vibration API</h2>

<p>Let’s turn our attention to the Vibration API, which, unsurprisingly, allows access to a device’s vibrating mechanism. This comes in handy when we need to alert users with in-app notifications, like when a process is finished or a message is received. That said, we have to use it sparingly; no one wants their phone blowing up with notifications.</p>

<p>There’s just one method that the Vibration API gives us, and it’s all we need: <code>navigator.vibrate()</code>.</p>

<p><code>vibrate()</code> is available globally from the <code>navigator</code> object and takes an argument for how long a vibration lasts in milliseconds. It can be either a number or an array of numbers representing a patron of vibrations and pauses.</p>

<div class="break-out">
<pre><code class="language-javascript">navigator.vibrate(200); // vibrate 200ms
navigator.vibrate([200, 100, 200]); // vibrate 200ms, wait 100, and vibrate 200ms.
</code></pre>
</div>

<h3 id="browser-support-2">Browser Support</h3>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://caniuse.com/vibration">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="290"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg"
			
			sizes="100vw"
			alt="Browser Support on Vibration API"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Source: <a href='https://caniuse.com/vibration'>Caniuse</a>. (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/vibration-api-support.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="vibration-api-demo">Vibration API Demo</h3>

<p>Let’s make a quick demo where the user inputs how many milliseconds they want their device to vibrate and buttons to start and stop the vibration, starting with the markup:</p>

<pre><code class="language-html">&lt;main&gt;
  &lt;form&gt;
    &lt;label for="milliseconds-input"&gt;Milliseconds:&lt;/label&gt;
    &lt;input type="number" id="milliseconds-input" value="0" /&gt;
  &lt;/form&gt;

  &lt;button class="vibrate-button"&gt;Vibrate&lt;/button&gt;
  &lt;button class="stop-vibrate-button"&gt;Stop&lt;/button&gt;
&lt;/main&gt;
</code></pre>

<p>We’ll add an event listener for a click and invoke the <code>vibrate()</code> method:</p>

<div class="break-out">
<pre><code class="language-javascript">const vibrateButton = document.querySelector(".vibrate-button");
const millisecondsInput = document.querySelector("#milliseconds-input");

vibrateButton.addEventListener("click", () =&gt; {
  navigator.vibrate(millisecondsInput.value);
});
</code></pre>
</div>

<p>To stop vibrating, we override the current vibration with a zero-millisecond vibration.</p>

<div class="break-out">
<pre><code class="language-javascript">const stopVibrateButton = document.querySelector(".stop-vibrate-button");

stopVibrateButton.addEventListener("click", () =&gt; {
  navigator.vibrate(0);
});
</code></pre>
</div>

<div class="partners__lead-place"></div>

<h2 id="4-contact-picker-api">4. Contact Picker API</h2>

<p>In the past, it used to be that only native apps could connect to a device’s “contacts”. But now we have the fourth and final API I want to look at: the <a href="https://w3c.github.io/contact-picker/"><strong>Contact Picker API</strong></a>.</p>

<p>The API grants web apps access to the device’s contact lists. Specifically, we get the <code>contacts.select()</code> async method available through the <code>navigator</code> object, which takes the following two arguments:</p>

<ul>
<li><strong><code>properties</code>:</strong> This is an array containing the information we want to fetch from a contact card, e.g., <code>&quot;name&quot;</code>, <code>&quot;address&quot;</code>, <code>&quot;email&quot;</code>, <code>&quot;tel&quot;</code>, and <code>&quot;icon&quot;</code>.</li>
<li><strong><code>options</code>:</strong> This is an object that can only contain the <code>multiple</code> boolean property to define whether or not the user can select one or multiple contacts at a time.</li>
</ul>

<h3 id="browser-support-3">Browser Support</h3>

<p>I’m afraid that browser support is next to zilch on this one, limited to Chrome Android, Samsung Internet, and Android’s native web browser at the time I’m writing this.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://caniuse.com/mdn-api_contactsmanager">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="301"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg"
			
			sizes="100vw"
			alt="Browser Support on Contacts Manager API"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Source: <a href='https://caniuse.com/mdn-api_contactsmanager'>Caniuse</a>. (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contacts-manager-api-support.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="selecting-user-s-contacts">Selecting User’s Contacts</h3>

<p>We will make another demo to select and display the user’s contacts on the page. Again, starting with the HTML:</p>

<pre><code class="language-html">&lt;main&gt;
  &lt;button class="get-contacts"&gt;Get Contacts&lt;/button&gt;
  &lt;p&gt;Contacts:&lt;/p&gt;
  &lt;ul class="contact-list"&gt;
    &lt;!-- We’ll inject a list of contacts --&gt;
  &lt;/ul&gt;
&lt;/main&gt;
</code></pre>

<p>Then, in JavaScript, we first construct our elements from the DOM and choose which properties we want to pick from the contacts.</p>

<div class="break-out">
<pre><code class="language-javascript">const getContactsButton = document.querySelector(".get-contacts");
const contactList = document.querySelector(".contact-list");

const props = ["name", "tel", "icon"];
const options = {multiple: true};
</code></pre>
</div>

<p>Now, we asynchronously pick the contacts when the user clicks the <code>getContactsButton</code>.</p>

<div class="break-out">
<pre><code class="language-javascript">
const getContacts = async () =&gt; {
  try {
    const contacts = await navigator.contacts.select(props, options);
  } catch (error) {
    console.error(error);
  }
};

getContactsButton.addEventListener("click", getContacts);
</code></pre>
</div>

<p>Using DOM manipulation, we can then append a list item to each contact and an icon to the <code>contactList</code> element.</p>

<div class="break-out">
<pre><code class="language-javascript">const appendContacts = (contacts) =&gt; {
  contacts.forEach(({name, tel, icon}) =&gt; {
    const contactElement = document.createElement("li");

    contactElement.innerText = `${name}: ${tel}`;
    contactList.appendChild(contactElement);
  });
};

const getContacts = async () =&gt; {
  try {
    const contacts = await navigator.contacts.select(props, options);
    appendContacts(contacts);
  } catch (error) {
    console.error(error);
  }
};

getContactsButton.addEventListener("click", getContacts);
</code></pre>
</div>

<p>Appending an image is a little tricky since we will need to convert it into a URL and append it for each item in the list.</p>

<div class="break-out">
<pre><code class="language-javascript">const getIcon = (icon) =&gt; {
  if (icon.length &gt; 0) {
    const imageUrl = URL.createObjectURL(icon[0]);
    const imageElement = document.createElement("img");
    imageElement.src = imageUrl;

    return imageElement;
  }
};

const appendContacts = (contacts) =&gt; {
  contacts.forEach(({name, tel, icon}) =&gt; {
    const contactElement = document.createElement("li");

    contactElement.innerText = `${name}: ${tel}`;
    contactList.appendChild(contactElement);

    const imageElement = getIcon(icon);
    contactElement.appendChild(imageElement);
  });
};

const getContacts = async () =&gt; {
  try {
    const contacts = await navigator.contacts.select(props, options);
    appendContacts(contacts);
  } catch (error) {
    console.error(error);
  }
};

getContactsButton.addEventListener("click", getContacts);
</code></pre>
</div>

<p>And here’s the outcome:</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="780"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png"
			
			sizes="100vw"
			alt="Contact Picker showing three mock contacts"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      (<a href='https://files.smashing.media/articles/uniting-web-native-apps-unknown-javascript-apis/contact-picker-1.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p><strong>Note</strong>: <strong><em>The Contact Picker API will only work if the context is secure</em></strong>, <em>i.e., the page is served over <code>https://</code> or <code>wss://</code> URLs.</em></p>

<h2 id="conclusion">Conclusion</h2>

<p>There we go, four web APIs that I believe would empower us to <strong>build more useful and robust PWAs</strong> but have slipped under the radar for many of us. This is, of course, due to inconsistent browser support, so I hope this article can bring awareness to new APIs so we have a better chance to see them in future browser updates.</p>

<p>Aren’t they interesting? We saw how much control we have with the orientation of a device and its screen as well as the level of access we get to access a device’s hardware features, i.e. vibration, and information from other apps to use in our own UI.</p>

<p>But as I said much earlier, there’s a sort of infinite loop where <strong>a lack of awareness begets a lack of browser support</strong>. So, while the four APIs we covered are super interesting, your mileage will inevitably vary when it comes to using them in a production environment. Please tread cautiously and refer to <a href="https://caniuse.com">Caniuse</a> for the latest support information, or check for your own devices using <a href="https://webapicheck.com/">WebAPI Check</a>.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Atila Fassina</author><title>The Era Of Platform Primitives Is Finally Here</title><link>https://www.smashingmagazine.com/2024/05/netlify-platform-primitives/</link><pubDate>Tue, 28 May 2024 12:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/05/netlify-platform-primitives/</guid><description>Application frameworks have built whole ecosystems on top of them. Let’s take a closer look at serverless platforms such as Netlify’s &lt;a href="https://www.netlify.com/platform/primitives">Platform Primitives&lt;/a> and explore how they can increase our productivity with a serverless fullstack experience.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/05/netlify-platform-primitives/" />
              <title>The Era Of Platform Primitives Is Finally Here</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>The Era Of Platform Primitives Is Finally Here</h1>
                  
                    
                    <address>Atila Fassina</address>
                  
                  <time datetime="2024-05-28T12:00:00&#43;00:00" class="op-published">2024-05-28T12:00:00+00:00</time>
                  <time datetime="2024-05-28T12:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                <p>This article is sponsored by <b>Netlify</b></p>
                

<p>In the past, the web ecosystem moved at a very slow pace. Developers would go years without a new language feature or working around a weird browser quirk. This pushed our technical leaders to come up with creative solutions to circumvent the platform’s shortcomings. We invented bundling, polyfills, and transformation steps to make things work everywhere with less of a hassle.</p>

<p>Slowly, we moved towards some sort of consensus on what we need as an ecosystem. We now have TypeScript and Vite as clear preferences—pushing the needle of what it means to build consistent experiences for the web. Application frameworks have built whole ecosystems on top of them: <a href="https://docs.solidjs.com/solid-start">SolidStart</a>, <a href="https://nuxtjs.org/">Nuxt</a>, <a href="https://remix.run">Remix</a>, and <a href="https://analogjs.org/">Analog</a> are examples of incredible tools built with such primitives. We can say that Vite and TypeScript are tooling primitives that empower the creation of others in diverse ecosystems.</p>

<p>With bundling and transformation needs somewhat defined, it was only natural that framework authors would move their gaze to the next layer they needed to abstract: the server.</p>

<h2 id="server-primitives">Server Primitives</h2>

<p>The UnJS folks have been consistently building agnostic tooling that can be reused in different ecosystems. Thanks to them, we now have frameworks and libraries such as <a href="https://h3.unjs.io/">H3</a> (a minimal Node.js server framework built with TypeScript), which enables <a href="https://nitro.unjs.io">Nitro</a> (a whole server runtime powered by <a href="https://vite.dev/">Vite</a>, and <a href="https://h3.unjs.io/">H3</a>), that in its own turn enabled Vinxi (an application bundler and server runtime that abstracts Nitro and Vite).</p>

<p><a href="https://nitro.unjs.io">Nitro</a> is used already by three major frameworks: <a href="https://nuxtjs.org/">Nuxt</a>, <a href="https://analogjs.org/">Analog</a>, and <a href="https://docs.solidjs.com/solid-start">SolidStart</a>. While Vinxi is also used by SolidStart. This means that any platform which supports one of these, will definitely be able to support the others with <strong>zero additional effort</strong>.</p>

<blockquote>This is not about taking a bigger slice of the cake. But making the cake bigger for everyone.</blockquote>

<p>Frameworks, platforms, developers, and users benefit from it. We bet on our ecosystem together instead of working in silos with our monolithic solutions. Empowering our developer-users to gain transferable skills and truly choose the best tool for the job with <strong>less vendor lock-in than ever before</strong>.</p>

<h2 id="serverless-rejoins-conversation">Serverless Rejoins Conversation</h2>

<p>Such initiatives have probably been noticed by serverless platforms like Netlify. With <a href="https://www.netlify.com/platform/primitives">Platform Primitives</a>, frameworks can leverage agnostic solutions for common necessities such as Incremental Static Regeneration (ISR), Image Optimization, and key/value (<code>kv</code>) storage.</p>

<p>As the name implies, <strong>Netlify Platform Primitives</strong> are a group of abstractions and helpers made available at a platform level for either frameworks or developers to leverage when using their applications. This brings additional functionality simultaneously to every framework. This is a big and powerful shift because, up until now, each framework would have to create its own solutions and backport such strategies to compatibility layers within each platform.</p>

<p>Moreover, developers would have to wait for a feature to first land on a framework and subsequently for support to arrive in their platform of choice. Now, as long as they’re using Netlify, those primitives are available directly without any effort and time put in by the framework authors. This empowers every ecosystem in a single measure.</p>

<blockquote>Serverless means server infrastructure developers don’t need to handle. It’s not a misnomer, but a format of <strong>Infrastructure As A Service</strong>.</blockquote>

<p>As mentioned before, <a href="https://www.netlify.com/platform/primitives">Netlify Platform Primitives</a> are three different features:</p>

<ol>
<li><strong>Image CDN</strong><br />
A <a href="https://www.netlify.com/blog/netlify-image-cdn-seamlessly-resize-crop-and-deliver-optimized-media-globally">content delivery network</a> for images. It can handle format transformation and size optimization via URL query strings.</li>
<li><strong>Caching</strong><br />
Basic primitives for their server runtime that help manage the caching directives for browser, server, and CDN runtimes smoothly.</li>
<li><strong>Blobs</strong><br />
A key/value (KV) storage option is automatically available to your project through their SDK.</li>
</ol>

<p>Let’s take a quick dive into each of these features and explore how they can increase our productivity with a serverless fullstack experience.</p>

<h3 id="image-cdn">Image CDN</h3>

<p>Every image in a <code>/public</code> can be served through a Netlify function. This means it’s possible to access it through a <code>/.netlify/images</code> path. So, without adding <a href="https://www.npmjs.com/package/sharp">sharp</a> or any image optimization package to your stack, deploying to <a href="https://www.netlify.com/platform">Netlify</a> allows us to serve our users with a better format without transforming assets at build-time. In a <a href="https://docs.solidjs.com/solid-start">SolidStart</a>, in a few lines of code, we could have an Image component that transforms other formats to <code>.webp</code>.</p>

<div class="break-out">
 <pre><code class="language-ts">import { type JSX } from "solid-js";

const SITE_URL = "https://example.com";

interface Props extends JSX.ImgHTMLAttributes&lt;HTMLImageElement&gt; {
  format?: "webp" | "jpeg" | "png" | "avif" | "preserve";
  quality?: number | "preserve";
}

const getQuality = (quality: Props["quality"]) => {
  if (quality === "preserve") return"";
  return `&q=${quality || "75"}`;
};

function getFormat(format: Props["format"]) {
  switch (format) {
    case "preserve":
      return"  ";
    case "jpeg":
      return `&fm=jpeg`;
    case "png":
      return `&fm=png`;
    case "avif":
      return `&fm=avif`;
    case "webp":
    default:
      return `&fm=webp`;
  }
}

export function Image(props: Props) {
  return (
    &lt;img
      {...props}
      src={`${SITE_URL}/.netlify/images?url=/${props.src}${getFormat(
        props.format
      )}${getQuality(props.quality)}`}
    /&gt;
  );
}
</code></pre>
</div>

<p>Notice the above component is even slightly more complex than bare essentials because we’re enforcing some default optimizations. Our <code>getFormat</code> method transforms images to <code>.webp</code> by default. It’s a broadly supported format that’s significantly smaller than the most common and without any loss in quality. Our <code>get quality</code> function reduces the image quality to 75% by default; as a rule of thumb, there isn’t any perceivable loss in quality for large images while still providing a significant size optimization.</p>

<ul>
<li><a href="https://primitives-test.netlify.app">Check our little component at play</a>.</li>
<li>Source code: <a href="https://github.com/atilafassina/primitives-test">SolidStart and Netlify Primitives</a>.</li>
</ul>

<h3 id="caching">Caching</h3>

<p>By default, Netlify caching is quite extensive for your regular artifacts - unless there’s a new deployment or the cache is flushed manually, resources will last for 365 days. However, because server/edge functions are dynamic in nature, there’s no default caching to prevent serving stale content to end-users. This means that if you have one of these functions in production, chances are there’s some caching to be leveraged to reduce processing time (and expenses).</p>

<p>By adding a cache-control header, you already have done 80% of the work in optimizing your resources for best serving users. Some commonly used cache control directives:</p>

<pre><code class="language-json">{
  "cache-control": "public, max-age=0, stale-while-revalidate=86400"

}</code></pre>

<ul>
<li><code>public</code>: Store in a shared cache.</li>
<li><code>max-age=0</code>: resource is immediately stale.</li>
<li><code>stale-while-revalidate=86400</code>: if the cache is stale for less than 1 day, return the cached value and revalidate it in the background.</li>
</ul>

<pre><code class="language-json">{
  "cache-control": "public, max-age=86400, must-revalidate"

}
</code></pre>

<ul>
<li><code>public</code>: Store in a shared cache.</li>
<li><code>max-age=86400</code>: resource is fresh for one day.</li>
<li><code>must-revalidate</code>: if a request arrives when the resource is already stale, the cache must be revalidated before a response is sent to the user.</li>
</ul>

<p><strong>Note</strong>: <em>For more extensive information about possible compositions of <code>Cache-Control</code> directives, check the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control">mdn entry on Cache-Control</a>.</em></p>

<p>The cache is a type of <strong>key/value storage</strong>. So, once our responses are set with proper cache control, platforms have some heuristics to define what the <code>key</code> will be for our resource within the cache storage. The Web Platform has a second very powerful header that can dictate how our cache behaves.</p>

<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary">Vary response header</a> is composed of a list of headers that will affect the validity of the resource (<code>method</code> and the endpoint URL are always considered; no need to add them). This header allows platforms to define other headers defined by location, language, and other patterns that will define for how long a response can be considered fresh.</p>

<p>The <strong>Vary</strong> response header is a foundational piece of a special header in <a href="https://docs.netlify.com/platform/caching/">Netlify Caching Primitive</a>. The <code>Netlify-Vary</code> will take a set of instructions on which parts of the request a key should be based. It is possible to tune a response key not only by the header but also by the <strong>value</strong> of the header.</p>

<ul>
<li><a href="https://docs.netlify.com/platform/caching/#vary-by-query-parameter">query</a>: vary by the value of some or all request query parameters.</li>
<li><a href="https://docs.netlify.com/platform/caching/#vary-by-header">header</a>: vary by the value of one or more request headers.</li>
<li><a href="https://docs.netlify.com/platform/caching/#vary-by-language">language</a>: vary by the languages from the <code>Accept-Language</code> header.</li>
<li><a href="https://docs.netlify.com/platform/caching/#vary-by-country">country</a>: vary by the country inferred from a GeoIP lookup on the request IP address.</li>
<li><a href="https://docs.netlify.com/platform/caching/#vary-by-cookie">cookie</a>: vary by the value of one or more request cookie keys.</li>
</ul>

<p>This header offers strong fine-control over how your resources are cached. Allowing for some creative strategies to optimize how your app will perform for specific users.</p>

<h3 id="blob-storage">Blob Storage</h3>

<p>This is a highly-available key/value store, it’s ideal for frequent reads and infrequent writes. They’re automatically available and provisioned for any Netlify Project.</p>

<p>It’s possible to write on a blob from your runtime or push data for a deployment-specific store. For example, this is how an <a href="https://docs.solidjs.com/solid-router/reference/data-apis/action">Action Function</a> would register a number of <strong>likes</strong> in store with SolidStart.</p>

<pre><code class="language-ts">import { getStore } from "@netlify/blobs";
import { action } from "@solidjs/router";

export const upVote = action(async (formData: FormData) => {
  "use server";

  const postId = formData.get("id");
  const postVotes = formData.get("votes");

  if (typeof postId !== "string" || typeof postVotes !== "string") return;

  const store = getStore("posts");
  const voteSum = Number(postVotes) + 1)
    
  await store.set(postId, String(voteSum);

  console.log("done");
  return voteSum
  
});
</code></pre>

<ul>
<li>Check <a href="https://docs.netlify.com/blobs/overview/#netlify-blobs-api"><code>@netlify/blobs</code> API documentation</a> for more examples and use-cases.</li>
</ul>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>With high-quality primitives, we can enable library and framework creators to create thin integration layers and adapters. This way, instead of focusing on how any specific platform operates, it will be possible to <strong>focus on the actual user experience</strong> and practical use-cases for such features. Monoliths and deeply integrated tooling make sense to build platforms fast with strong vendor lock-in, but that’s not what the community needs. Betting on the web platform is a more sensible and future-friendly way.</p>

<p>Let me know in the comments what your take is about unbiased tooling versus opinionated setups!</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(il)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Lazar Nikolov</author><title>The Forensics Of React Server Components (RSCs)</title><link>https://www.smashingmagazine.com/2024/05/forensics-react-server-components/</link><pubDate>Thu, 09 May 2024 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/05/forensics-react-server-components/</guid><description>We love client-side rendering for the way it relieves the server of taxing operations, but serving an empty HTML page often leads to taxing user experiences during the initial page load. We love server-side rendering because it allows us to serve static assets on speedy CDNs, but they’re unfit for large-scale projects with dynamic content. React Server Components (RSCs) combine the best of both worlds, and author Lazar Nikolov thoroughly examines how we got here with a deep look at the impact that RSCs have on the page load timeline.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/05/forensics-react-server-components/" />
              <title>The Forensics Of React Server Components (RSCs)</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>The Forensics Of React Server Components (RSCs)</h1>
                  
                    
                    <address>Lazar Nikolov</address>
                  
                  <time datetime="2024-05-09T13:00:00&#43;00:00" class="op-published">2024-05-09T13:00:00+00:00</time>
                  <time datetime="2024-05-09T13:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                <p>This article is sponsored by <b>Sentry.io</b></p>
                

<p>In this article, we’re going to look deeply at React Server Components (RSCs). They are the latest innovation in React’s ecosystem, leveraging both server-side and client-side rendering as well as <a href="https://en.wikipedia.org/wiki/Chunked_transfer_encoding">streaming HTML</a> to deliver content as fast as possible.</p>

<p>We will get really nerdy to get a full understanding of how RSCs fit into the React picture, the level of control they offer over the rendering lifecycle of components, and what page loads look like with RSCs in place.</p>

<p>But before we dive into all of that, I think it’s worth looking back at how React has rendered websites up until this point to set the context for why we need RSCs in the first place.</p>

<h2 id="the-early-days-react-client-side-rendering">The Early Days: React Client-Side Rendering</h2>

<p>The first React apps were rendered on the client side, i.e., in the browser. As developers, we wrote apps with JavaScript classes as components and packaged everything up using bundlers, like Webpack, in a nicely compiled and tree-shaken heap of code ready to ship in a production environment.</p>

<p>The HTML that returned from the server contained a few things, including:</p>

<ul>
<li>An HTML document with metadata in the <code>&lt;head&gt;</code> and a blank <code>&lt;div&gt;</code> in the <code>&lt;body&gt;</code> used as a hook to inject the app into the DOM;</li>
<li>JavaScript resources containing React’s core code and the actual code for the web app, which would generate the user interface and populate the app inside of the empty <code>&lt;div&gt;</code>.</li>
</ul>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/1-client-side-rendering-process.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="566"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/1-client-side-rendering-process.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/1-client-side-rendering-process.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/1-client-side-rendering-process.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/1-client-side-rendering-process.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/1-client-side-rendering-process.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/1-client-side-rendering-process.jpg"
			
			sizes="100vw"
			alt="Diagram of the client-side rendering process of a React app, starting with a blank loading page in the browser followed by a series of processes connected to CDNs and APIs to produce content on the loading page."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 1. (<a href='https://files.smashing.media/articles/forensics-react-server-components/1-client-side-rendering-process.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>A web app under this process is only fully interactive once JavaScript has fully completed its operations. You can probably already see the tension here that comes with an <strong>improved developer experience (DX) that negatively impacts the user experience (UX)</strong>.</p>

<p>The truth is that there were (and are) pros and cons to CSR in React. Looking at the positives, web applications delivered <strong>smooth, quick transitions</strong> that reduced the overall time it took to load a page, thanks to reactive components that update with user interactions without triggering page refreshes. CSR lightens the server load and allows us to serve assets from speedy content delivery networks (CDNs) capable of delivering content to users from a server location geographically closer to the user for even more optimized page loads.</p>

<p>There are also not-so-great consequences that come with CSR, most notably perhaps that components could fetch data independently, leading to <a href="https://blog.sentry.io/fetch-waterfall-in-react/"><strong>waterfall network requests</strong></a> that dramatically slow things down. This may sound like a minor nuisance on the UX side of things, but the damage can actually be quite large on a human level. Eric Bailey’s “<a href="https://ericwbailey.design/published/modern-health-frameworks-performance-and-harm/">Modern Health, frameworks, performance, and harm</a>” should be a cautionary tale for all CSR work.</p>

<p>Other negative CSR consequences are not quite as severe but still lead to damage. For example, it used to be that an HTML document containing nothing but metadata and an empty <code>&lt;div&gt;</code> was illegible to search engine crawlers that never get the fully-rendered experience. While that’s solved today, the SEO hit at the time was an anchor on company sites that rely on search engine traffic to generate revenue.</p>

<h2 id="the-shift-server-side-rendering-ssr">The Shift: Server-Side Rendering (SSR)</h2>

<p>Something needed to change. CSR presented developers with a powerful new approach for constructing speedy, interactive interfaces, but users everywhere were inundated with blank screens and loading indicators to get there. The solution was to move the rendering experience from the <strong>client</strong> to the <strong>server</strong>. I know it sounds funny that we needed to improve something by going back to the way it was before.</p>

<p>So, yes, React gained server-side rendering (SSR) capabilities. At one point, SSR was such a topic in the React community that <a href="https://sentry.io/resources/moving-to-server-side-rendering/">it had a moment</a> in the spotlight. The move to SSR brought significant changes to app development, specifically in how it influenced React behavior and how content could be delivered by way of servers instead of browsers.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/2-diagram-server-side-rendering-process.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="600"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/2-diagram-server-side-rendering-process.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/2-diagram-server-side-rendering-process.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/2-diagram-server-side-rendering-process.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/2-diagram-server-side-rendering-process.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/2-diagram-server-side-rendering-process.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/2-diagram-server-side-rendering-process.jpg"
			
			sizes="100vw"
			alt="Diagram of the server-side rendering process of a React app, starting with a blank loading page in the browser followed by a screen of un-interactive content, then a fully interactive page of content."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 2. (<a href='https://files.smashing.media/articles/forensics-react-server-components/2-diagram-server-side-rendering-process.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="addressing-csr-limitations">Addressing CSR Limitations</h3>

<p>Instead of sending a blank HTML document with SSR, we rendered the initial HTML on the server and sent it to the browser. The browser was able to immediately start displaying the content without needing to show a loading indicator. This significantly improves the <a href="https://docs.sentry.io/product/performance/web-vitals/web-vitals-concepts/#first-contentful-paint-fcp">First Contentful Paint (FCP) performance metric in Web Vitals</a>.</p>

<p>Server-side rendering also fixed the SEO issues that came with CSR. Since the crawlers received the content of our websites directly, they were then able to index it right away. The data fetching that happens initially also takes place on the server, which is a plus because it’s closer to the data source and can eliminate fetch waterfalls <a href="https://blog.sentry.io/fetch-waterfall-in-react/#fetch-data-on-server-to-avoid-a-fetch-waterfall"><em>if done properly</em></a>.</p>

<h3 id="hydration">Hydration</h3>

<p>SSR has its own complexities. For React to make the static HTML received from the server interactive, it needs to <strong>hydrate</strong> it. Hydration is the process that happens when React reconstructs its Virtual Document Object Model (DOM) on the client side based on what was in the DOM of the initial HTML.</p>

<blockquote><strong>Note</strong>: React maintains its own <a href="https://legacy.reactjs.org/docs/faq-internals.html">Virtual DOM</a> because it’s faster to figure out updates on it instead of the actual DOM. It synchronizes the actual DOM with the Virtual DOM when it needs to update the UI but performs the diffing algorithm on the Virtual DOM.</blockquote>

<p>We now have two flavors of Reacts:</p>

<ol>
<li><strong>A server-side flavor</strong> that knows how to render static HTML from our component tree,</li>
<li><strong>A client-side flavor</strong> that knows how to make the page interactive.</li>
</ol>

<p>We’re still shipping React and code for the app to the browser because &mdash; in order to hydrate the initial HTML &mdash; React needs the same components on the client side that were used on the server. During hydration, <a href="https://css-tricks.com/how-react-reconciliation-works/">React performs a process called</a> <a href="https://css-tricks.com/how-react-reconciliation-works/"><em>reconciliation</em></a> in which it compares the server-rendered DOM with the client-rendered DOM and tries to identify differences between the two. If there are differences between the two DOMs, React attempts to fix them by rehydrating the component tree and updating the component hierarchy to match the server-rendered structure. And if there are <em>still</em> inconsistencies that cannot be resolved, React will throw errors to indicate the problem. This problem is commonly known as a <em>hydration error</em>.</p>

<h3 id="ssr-drawbacks">SSR Drawbacks</h3>

<p>SSR is not a silver bullet solution that addresses CSR limitations. SSR comes with its own drawbacks. Since we moved the initial HTML rendering and data fetching to the server, those servers are now experiencing a much greater load than when we loaded everything on the client.</p>

<p>Remember when I mentioned that SSR generally improves the FCP performance metric? That may be true, but the <a href="https://docs.sentry.io/product/performance/web-vitals/web-vitals-concepts/#time-to-first-byte-ttfb">Time to First Byte (TTFB) performance metric</a> took a negative hit with SSR. The browser literally has to wait for the server to fetch the data it needs, generate the initial HTML, and send the first byte. And while TTFB is not a Core Web Vital metric in itself, it influences the metrics. A negative TTFB leads to negative Core Web Vitals metrics.</p>

<p>Another drawback of SSR is that the entire page is unresponsive until client-side React has finished hydrating it. Interactive elements cannot listen and “react” to user interactions before React hydrates them, i.e., React attaches the intended event listeners to them. The hydration process is typically fast, but the internet connection and hardware capabilities of the device in use can slow down rendering by a noticeable amount.</p>

<h2 id="the-present-a-hybrid-approach">The Present: A Hybrid Approach</h2>

<p>So far, we have covered two different flavors of React rendering: CSR and SSR. While the two were attempts to improve one another, we now get the best of both worlds, so to speak, as SSR has branched into three additional React flavors that offer a hybrid approach in hopes of reducing the limitations that come with CSR and SSR.</p>

<p>We’ll look at the first two &mdash; <strong>static site generation</strong> and <strong>incremental static regeneration</strong> &mdash; before jumping into an entire discussion on React Server Components, the third flavor.</p>

<h3 id="static-site-generation-ssg">Static Site Generation (SSG)</h3>

<p>Instead of regenerating the same HTML code on every request, we came up with SSG. This React flavor compiles and builds the entire app at build time, generating static (as in vanilla HTML and CSS) files that are, in turn, hosted on a speedy CDN.</p>

<p>As you might suspect, this hybrid approach to rendering is a nice fit for smaller projects where the content doesn’t change much, like a marketing site or a personal blog, as opposed to larger projects where content may change with user interactions, like an e-commerce site.</p>

<p>SSG reduces the burden on the server while improving performance metrics related to TTFB because the server no longer has to perform heavy, expensive tasks for re-rendering the page.</p>

<h3 id="incremental-static-regeneration-isr">Incremental Static Regeneration (ISR)</h3>

<p>One SSG drawback is having to rebuild all of the app’s code when a content change is needed. The content is set in stone &mdash; being static and all &mdash; and there’s no way to change just one part of it without rebuilding the whole thing.</p>

<p>The Next.js team created the second hybrid flavor of React that addresses the drawback of complete SSG rebuilds: <strong>incremental static regeneration (ISR)</strong>. The name says a lot about the approach in that ISR only rebuilds what’s needed instead of the entire thing. We generate the “initial version” of the page statically during build time but are also able to rebuild any page containing stale data <em>after</em> a user lands on it (i.e., the server request triggers the data check).</p>

<p>From that point on, the server will serve new versions of that page statically in increments when needed. That makes ISR a hybrid approach that is neatly positioned between SSG and traditional SSR.</p>

<p>At the same time, ISR does not address the “stale content” symptom, where users may visit a page before it has finished being generated. Unlike SSG, ISR needs an actual server to regenerate individual pages in response to a user’s browser making a server request. That means we lose the valuable ability to deploy ISR-based apps on a CDN for optimized asset delivery.</p>

<h2 id="the-future-react-server-components">The Future: React Server Components</h2>

<p>Up until this point, we’ve juggled between CSR, SSR, SSG, and ISR approaches, where all make some sort of trade-off, negatively affecting performance, development complexity, and user experience. Newly introduced <a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">React Server Components</a> (RSC) aim to address most of these drawbacks by allowing us &mdash; the developer &mdash; to <strong>choose the right rendering strategy for each individual React component</strong>.</p>

<p>RSCs can significantly reduce the amount of JavaScript shipped to the client since we can selectively decide which ones to serve statically on the server and which render on the client side. There’s a lot more control and flexibility for striking the right balance for your particular project.</p>

<blockquote><strong>Note:</strong> It’s important to keep in mind that as we adopt more advanced architectures, like RSCs, monitoring solutions become invaluable. Sentry offers robust <a href="https://docs.sentry.io/product/performance/">performance monitoring</a> and error-tracking capabilities that help you keep an eye on the real-world performance of your RSC-powered application. Sentry also helps you gain insights into how your releases are performing and how stable they are, which is yet another crucial feature to have while migrating your existing applications to RSCs. Implementing Sentry in an RSC-enabled framework like <a href="https://sentry.io/for/nextjs/">Next.js</a> is as easy as running a single terminal command.</blockquote>

<p>But what exactly <em>is</em> an RSC? Let’s pick one apart to see how it works under the hood.</p>

<h2 id="the-anatomy-of-react-server-components">The Anatomy of React Server Components</h2>

<p>This new approach introduces two types of rendering components: <strong>Server Components</strong> and <strong>Client Components</strong>. The differences between these two are not <em>how</em> they function but <em>where</em> they execute and the environments they’re designed for. At the time of this writing, the only way to use RSCs is through React frameworks. And at the moment, there are only three frameworks that support them: <a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">Next.js</a>, <a href="https://www.gatsbyjs.com/docs/conceptual/partial-hydration/">Gatsby</a>, and <a href="https://redwoodjs.com/blog/rsc-now-in-redwoodjs">RedwoodJS</a>.</p>














<figure class="
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/3-wire-diagram-server-client-components.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="763"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/3-wire-diagram-server-client-components.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/3-wire-diagram-server-client-components.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/3-wire-diagram-server-client-components.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/3-wire-diagram-server-client-components.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/3-wire-diagram-server-client-components.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/3-wire-diagram-server-client-components.jpg"
			
			sizes="100vw"
			alt="Wire diagram showing connected server components and client components represented as gray and blue dots, respectively."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 3: Example of an architecture consisting of Server Components and Client Components. (<a href='https://files.smashing.media/articles/forensics-react-server-components/3-wire-diagram-server-client-components.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<h3 id="server-components">Server Components</h3>

<p>Server Components are designed to be executed on the server, and their code is never shipped to the browser. The HTML output and any props they might be accepting are the only pieces that are served. This approach has multiple performance benefits and user experience enhancements:</p>

<ul>
<li><strong>Server Components allow for large dependencies to remain on the server side.</strong><br />
Imagine using a large library for a component. If you’re executing the component on the client side, it means that you’re also shipping the full library to the browser. With Server Components, you’re only taking the static HTML output and avoiding having to ship any JavaScript to the browser. Server Components are truly static, and they remove the whole hydration step.</li>
<li><strong>Server Components are located much closer to the data sources &mdash; e.g., databases or file systems &mdash; they need to generate code.</strong><br />
They also leverage the server’s computational power to speed up compute-intensive rendering tasks and send only the generated results back to the client. They are also generated in a single pass, which <a href="https://blog.sentry.io/fetch-waterfall-in-react/#fetch-data-on-server-to-avoid-a-fetch-waterfall">avoids request waterfalls and HTTP round trips</a>.</li>
<li><strong>Server Components safely keep sensitive data and logic away from the browser.</strong><br />
That’s thanks to the fact that personal tokens and API keys are executed on a secure server rather than the client.</li>
<li><strong>The rendering results can be cached and reused between subsequent requests and even across different sessions.</strong><br />
This significantly reduces rendering time, as well as the overall amount of data that is fetched for each request.</li>
</ul>

<p>This architecture also makes use of <strong>HTML streaming</strong>, which means the server defers generating HTML for specific components and instead renders a fallback element in their place while it works on sending back the generated HTML. Streaming Server Components wrap components in <a href="https://react.dev/reference/react/Suspense"><code>&lt;Suspense&gt;</code></a> tags that provide a fallback value. The implementing framework uses the fallback initially but streams the newly generated content when it‘s ready. We’ll talk more about streaming, but let’s first look at Client Components and compare them to Server Components.</p>

<h3 id="client-components">Client Components</h3>

<p>Client Components are the components we already know and love. They’re executed on the client side. Because of this, Client Components are capable of handling user interactions and have access to the browser APIs like <code>localStorage</code> and geolocation.</p>

<p>The term “Client Component” doesn’t describe anything new; they merely are given the label to help distinguish the “old” CSR components from Server Components. Client Components are defined by a <a href="https://react.dev/reference/react/use-server"><code>&quot;use client&quot;</code></a> directive at the top of their files.</p>

<pre><code class="language-javascript">"use client"
export default function LikeButton() {
  const likePost = () =&gt; {
    // ...
  }
  return (
    &lt;button onClick={likePost}&gt;Like&lt;/button&gt;
  )
}
</code></pre>

<p>In Next.js, all components are Server Components by default. That’s why we need to explicitly define our Client Components with <code>&quot;use client&quot;</code>. There’s also a <code>&quot;use server&quot;</code> directive, but it’s used for Server Actions (which are RPC-like actions that invoked from the client, but executed on the server). You don’t use it to define your Server Components.</p>

<p>You might (rightfully) assume that Client Components are only rendered on the client, but Next.js renders Client Components on the server to generate the initial HTML. As a result, browsers can immediately start rendering them and then perform hydration later.</p>

<h3 id="the-relationship-between-server-components-and-client-components">The Relationship Between Server Components and Client Components</h3>

<p>Client Components can only <em>explicitly</em> import other Client Components. In other words, we’re unable to import a Server Component into a Client Component because of re-rendering issues. But we can have Server Components in a Client Component’s subtree &mdash; only passed through the <code>children</code> prop. Since Client Components live in the browser and they handle user interactions or define their own state, they get to re-render often. When a Client Component re-renders, so will its subtree. But if its subtree contains Server Components, how would they re-render? They don’t live on the client side. That’s why the React team put that limitation in place.</p>

<p>But hold on! We actually <em>can</em> import Server Components into Client Components. It’s just not a direct one-to-one relationship because the Server Component will be converted into a Client Component. If you’re using server APIs that you can’t use in the browser, you’ll get an error; if not &mdash; you’ll have a Server Component whose code gets “leaked” to the browser.</p>

<p>This is an incredibly important nuance to keep in mind as you work with RSCs.</p>

<h2 id="the-rendering-lifecycle">The Rendering Lifecycle</h2>

<p>Here’s the order of operations that Next.js takes to stream contents:</p>

<ol>
<li>The app router matches the page’s URL to a Server Component, builds the component tree, and instructs the server-side React to render that Server Component and all of its children components.</li>
<li>During render, React generates an “RSC Payload”. The RSC Payload informs Next.js about the page and what to expect in return, as well as what to fall back to during a <code>&lt;Suspense&gt;</code>.</li>
<li>If React encounters a suspended component, it pauses rendering that subtree and uses the suspended component’s fallback value.</li>
<li>When React loops through the last static component, Next.js prepares the generated HTML and the RSC Payload before streaming it back to the client through one or multiple chunks.</li>
<li>The client-side React then uses the instructions it has for the RSC Payload and client-side components to render the UI. It also hydrates each Client Component as they load.</li>
<li>The server streams in the suspended Server Components as they become available as an RSC Payload. Children of Client Components are also hydrated at this time if the suspended component contains any.</li>
</ol>

<p>We will look at the RSC rendering lifecycle from the browser’s perspective momentarily. For now, the following figure illustrates the outlined steps we covered.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/4-wire-diagram-rsc-rendering-lifecycle.jpg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="489"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/4-wire-diagram-rsc-rendering-lifecycle.jpg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/4-wire-diagram-rsc-rendering-lifecycle.jpg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/4-wire-diagram-rsc-rendering-lifecycle.jpg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/4-wire-diagram-rsc-rendering-lifecycle.jpg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/4-wire-diagram-rsc-rendering-lifecycle.jpg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/4-wire-diagram-rsc-rendering-lifecycle.jpg"
			
			sizes="100vw"
			alt="Wire diagram of the RSC rendering lifecycle going from a blank page to a page shell to a complete page."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 4: Diagram of the RSC Rendering Lifecycle. (<a href='https://files.smashing.media/articles/forensics-react-server-components/4-wire-diagram-rsc-rendering-lifecycle.jpg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>We’ll see this operation flow from the browser’s perspective in just a bit.</p>

<h2 id="rsc-payload">RSC Payload</h2>

<p>The RSC payload is a special data format that the server generates as it renders the component tree, and it includes the following:</p>

<ul>
<li>The rendered HTML,</li>
<li>Placeholders where the Client Components should be rendered,</li>
<li>References to the Client Components’ JavaScript files,</li>
<li>Instructions on which JavaScript files it should invoke,</li>
<li>Any props passed from a Server Component to a Client Component.</li>
</ul>

<p>There’s no reason to worry much about the RSC payload, but it’s worth understanding what exactly the RSC payload contains. Let’s examine an example (truncated for brevity) from a <a href="https://github.com/nikolovlazar/rsc-forensics">demo app I created</a>:</p>

<div class="break-out">
<pre><code class="language-javascript">1:HL["/&#95;next/static/media/c9a5bc6a7c948fb0-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
2:HL["/&#95;next/static/css/app/layout.css?v=1711137019097","style"]
0:"$L3"
4:HL["/&#95;next/static/css/app/page.css?v=1711137019097","style"]
5:I["(app-pages-browser)/./node&#95;modules/next/dist/client/components/app-router.js",["app-pages-internals","static/chunks/app-pages-internals.js"],""]
8:"$Sreact.suspense"
a:I["(app-pages-browser)/./node&#95;modules/next/dist/client/components/layout-router.js",["app-pages-internals","static/chunks/app-pages-internals.js"],""]
b:I["(app-pages-browser)/./node&#95;modules/next/dist/client/components/render-from-template-context.js",["app-pages-internals","static/chunks/app-pages-internals.js"],""]
d:I["(app-pages-browser)/./src/app/global-error.jsx",["app/global-error","static/chunks/app/global-error.js"],""]
f:I["(app-pages-browser)/./src/components/clearCart.js",["app/page","static/chunks/app/page.js"],"ClearCart"]
7:["$","main",null,{"className":"page&#95;main&#95;&#95;GlU4n","children":[["$","$Lf",null,{}],["$","$8",null,{"fallback":["$","p",null,{"children":"🌀 loading products..."}],"children":"$L10"}]]}]
c:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}]...
9:["$","p",null,{"children":["🛍️ ",3]}]
11:I["(app-pages-browser)/./src/components/addToCart.js",["app/page","static/chunks/app/page.js"],"AddToCart"]
10:["$","ul",null,{"children":[["$","li","1",{"children":["Gloves"," - $",20,["$...
</code></pre>
</div>

<p>To find this code in the demo app, open your browser’s developer tools at the Elements tab and look at the <code>&lt;script&gt;</code> tags at the bottom of the page. They’ll contain lines like:</p>

<pre><code class="language-javascript">self.&#95;&#95;next&#95;f.push([1,"PAYLOAD&#95;STRING&#95;HERE"]).
</code></pre>

<p>Every line from the snippet above is an individual RSC payload. You can see that each line starts with a number or a letter, followed by a colon, and then an array that’s sometimes prefixed with letters. We won’t get into too deep in detail as to what they mean, but in general:</p>

<ul>
<li><strong><code>HL</code> payloads</strong> are called “hints” and link to specific resources like CSS and fonts.</li>
<li><strong><code>I</code> payloads</strong> are called “modules,” and they invoke specific scripts. This is how Client Components are being loaded as well. If the Client Component is part of the main bundle, it’ll execute. If it’s not (meaning it’s lazy-loaded), a fetcher script is added to the main bundle that fetches the component’s CSS and JavaScript files when it needs to be rendered. There’s going to be an <code>I</code> payload sent from the server that invokes the fetcher script when needed.</li>
<li><strong><code>&quot;$&quot;</code> payloads</strong> are DOM definitions generated for a certain Server Component. They are usually accompanied by actual static HTML streamed from the server. That’s what happens when a suspended component becomes ready to be rendered: the server generates its static HTML and RSC Payload and then streams both to the browser.</li>
</ul>

<h2 id="streaming">Streaming</h2>

<p>Streaming allows us to progressively render the UI from the server. With RSCs, each component is capable of fetching its own data. Some components are fully static and ready to be sent immediately to the client, while others require more work before loading. Based on this, Next.js splits that work into multiple chunks and streams them to the browser as they become ready. So, when a user visits a page, the server invokes all Server Components, generates the initial HTML for the page (i.e., the page shell), replaces the “suspended” components’ contents with their fallbacks, and streams all of that through one or multiple chunks back to the client.</p>

<p>The server returns a <code>Transfer-Encoding: chunked</code> header that lets the browser know to expect streaming HTML. This prepares the browser for receiving multiple chunks of the document, rendering them as it receives them. We can actually see the header when opening Developer Tools at the Network tab. Trigger a refresh and click on the document request.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/5-streaming-header.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="238"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/5-streaming-header.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/5-streaming-header.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/5-streaming-header.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/5-streaming-header.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/5-streaming-header.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/5-streaming-header.jpeg"
			
			sizes="100vw"
			alt="Response header output highlighting the line containing the chunked transfer endcoding"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 5: Providing a hint to the browser to expect HTML streaming. (<a href='https://files.smashing.media/articles/forensics-react-server-components/5-streaming-header.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>We can also debug the way Next.js sends the chunks in a terminal with the <code>curl</code> command:</p>

<pre><code class="language-bash">curl -D - --raw localhost:3000 &gt; chunked-response.txt
</code></pre>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/6-chunked-response.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="416"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/6-chunked-response.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/6-chunked-response.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/6-chunked-response.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/6-chunked-response.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/6-chunked-response.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/6-chunked-response.jpeg"
			
			sizes="100vw"
			alt="Headers and chunked HTML payloads."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 6. (<a href='https://files.smashing.media/articles/forensics-react-server-components/6-chunked-response.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>You probably see the pattern. For each chunk, the server responds with the chunk’s size before sending the chunk’s contents. Looking at the output, we can see that the server streamed the entire page in 16 different chunks. At the end, the server sends back a zero-sized chunk, indicating the end of the stream.</p>

<p>The first chunk starts with the <code>&lt;!DOCTYPE html&gt;</code> declaration. The second-to-last chunk, meanwhile, contains the closing <code>&lt;/body&gt;</code> and <code>&lt;/html&gt;</code> tags. So, we can see that the server streams the entire document from top to bottom, then pauses to wait for the suspended components, and finally, at the end, closes the body and HTML before it stops streaming.</p>

<p>Even though the server hasn’t completely finished streaming the document, the browser’s fault tolerance features allow it to draw and invoke whatever it has at the moment without waiting for the closing <code>&lt;/body&gt;</code> and <code>&lt;/html&gt;</code> tags.</p>

<h3 id="suspending-components">Suspending Components</h3>

<p>We learned from the render lifecycle that when a page is visited, Next.js matches the RSC component for that page and asks React to render its subtree in HTML. When React stumbles upon a suspended component (i.e., async function component), it grabs its fallback value from the <code>&lt;Suspense&gt;</code> component (or the <code>loading.js</code> file if it’s a Next.js route), renders that instead, then continues loading the other components. Meanwhile, the RSC invokes the async component in the background, which is streamed later as it finishes loading.</p>

<p>At this point, Next.js has returned a full page of static HTML that includes either the components themselves (rendered in static HTML) or their fallback values (if they’re suspended). It takes the static HTML and RSC payload and streams them back to the browser through one or multiple chunks.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/7-fallbacks-suspended-components.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/7-fallbacks-suspended-components.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/7-fallbacks-suspended-components.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/7-fallbacks-suspended-components.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/7-fallbacks-suspended-components.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/7-fallbacks-suspended-components.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/7-fallbacks-suspended-components.jpeg"
			
			sizes="100vw"
			alt="Showing suspended component fallbacks"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 7. (<a href='https://files.smashing.media/articles/forensics-react-server-components/7-fallbacks-suspended-components.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>As the suspended components finish loading, React generates HTML recursively while looking for other nested <code>&lt;Suspense&gt;</code> boundaries, generates their RSC payloads and then lets Next.js stream the HTML and RSC Payload back to the browser as new chunks. When the browser receives the new chunks, it has the HTML and RSC payload it needs and is ready to replace the fallback element from the DOM with the newly-streamed HTML. And so on.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/8-suspended-components-html.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="399"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/8-suspended-components-html.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/8-suspended-components-html.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/8-suspended-components-html.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/8-suspended-components-html.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/8-suspended-components-html.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/8-suspended-components-html.jpeg"
			
			sizes="100vw"
			alt="Static HTML and RSC Payload replacing suspended fallback values."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 8. (<a href='https://files.smashing.media/articles/forensics-react-server-components/8-suspended-components-html.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>In Figures 7 and 8, notice how the fallback elements have a unique ID in the form of <code>B:0</code>, <code>B:1</code>, and so on, while the actual components have a similar ID in a similar form: <code>S:0</code> and <code>S:1</code>, and so on.</p>

<p>Along with the first chunk that contains a suspended component’s HTML, the server also ships an <code>$RC</code> function (i.e., <code>completeBoundary</code> from <a href="https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js#L46">React’s source code</a>) that knows how to find the <code>B:0</code> fallback element in the DOM and replace it with the <code>S:0</code> template it received from the server. That’s the “replacer” function that lets us see the component contents when they arrive in the browser.</p>

<p>The entire page eventually finishes loading, chunk by chunk.</p>

<h3 id="lazy-loading-components">Lazy-Loading Components</h3>

<p>If a suspended Server Component contains a lazy-loaded Client Component, Next.js will also send an RSC payload chunk containing instructions on how to fetch and load the lazy-loaded component’s code. This represents a <em>significant performance improvement</em> because the page load isn’t dragged out by JavaScript, which might not even be loaded during that session.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/9-fetching-lazy-loaded-scripts.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="442"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/9-fetching-lazy-loaded-scripts.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/9-fetching-lazy-loaded-scripts.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/9-fetching-lazy-loaded-scripts.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/9-fetching-lazy-loaded-scripts.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/9-fetching-lazy-loaded-scripts.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/9-fetching-lazy-loaded-scripts.jpeg"
			
			sizes="100vw"
			alt="Fetching additional JavaScript and CSS files for a lazy-loaded Client Component, as shown in developer tools."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 9. (<a href='https://files.smashing.media/articles/forensics-react-server-components/9-fetching-lazy-loaded-scripts.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>At the time I’m writing this, the dynamic method to lazy-load a Client Component in a Server Component in Next.js does not work as you might expect. To effectively lazy-load a Client Component, put it in a <a href="https://github.com/nikolovlazar/rsc-forensics/blob/main/src/components/addToCartWrapper.js">“wrapper” Client Component</a> that uses the <code>dynamic</code> method itself to lazy-load the actual Client Component. The wrapper will be turned into a script that fetches and loads the Client Component’s JavaScript and CSS files at the time they’re needed.</p>

<h3 id="tl-dr">TL;DR</h3>

<p>I know that’s a lot of plates spinning and pieces moving around at various times. What it boils down to, however, is that a page visit triggers Next.js to render as much HTML as it can, using the fallback values for any suspended components, and then sends that to the browser. Meanwhile, Next.js triggers the suspended async components and gets them formatted in HTML and contained in RSC Payloads that are streamed to the browser, one by one, along with an <code>$RC</code> script that knows how to swap things out.</p>

<h2 id="the-page-load-timeline">The Page Load Timeline</h2>

<p>By now, we should have a solid understanding of how RSCs work, how Next.js handles their rendering, and how all the pieces fit together. In this section, we’ll zoom in on what exactly happens when we visit an RSC page in the browser.</p>

<h3 id="the-initial-load">The Initial Load</h3>

<p>As we mentioned in the TL;DR section above, when visiting a page, Next.js will render the initial HTML minus the suspended component and stream it to the browser as part of the first streaming chunks.</p>

<p>To see everything that happens during the page load, we’ll visit the “Performance” tab in Chrome DevTools and click on the “reload” button to reload the page and capture a profile. Here’s what that looks like:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/10-first-chunks-being-streamed.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="442"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/10-first-chunks-being-streamed.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/10-first-chunks-being-streamed.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/10-first-chunks-being-streamed.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/10-first-chunks-being-streamed.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/10-first-chunks-being-streamed.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/10-first-chunks-being-streamed.jpeg"
			
			sizes="100vw"
			alt="Showing the first chunks of HTML streamed at the beginning of the timeline in DevTools."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 10. (<a href='https://files.smashing.media/articles/forensics-react-server-components/10-first-chunks-being-streamed.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>When we zoom in at the very beginning, we can see the first “Parse HTML” span. That’s the server streaming the first chunks of the document to the browser. The browser has just received the initial HTML, which contains the page shell and a few links to resources like fonts, CSS files, and JavaScript. The browser starts to invoke the scripts.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/11-first-frames.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="442"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/11-first-frames.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/11-first-frames.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/11-first-frames.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/11-first-frames.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/11-first-frames.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/11-first-frames.jpeg"
			
			sizes="100vw"
			alt="The first frames appear, and parts of the page are rendered"
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 11. (<a href='https://files.smashing.media/articles/forensics-react-server-components/11-first-frames.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>After some time, we start to see the page’s first frames appear, along with the initial JavaScript scripts being loaded and hydration taking place. If you look at the frame closely, you’ll see that the whole page shell is rendered, and “loading” components are used in the place where there are suspended Server Components. You might notice that this takes place around 800ms, while the browser started to get the first HTML at  100ms. During those 700ms, the browser is continuously receiving chunks from the server.</p>

<p>Bear in mind that this is a Next.js demo app running locally in development mode, so it’s going to be slower than when it’s running in production mode.</p>

<h3 id="the-suspended-component">The Suspended Component</h3>

<p>Fast forward few seconds and we see another “Parse HTML” span in the page load timeline, but this one it indicates that a suspended Server Component finished loading and is being streamed to the browser.</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/12-suspended-component.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="442"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/12-suspended-component.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/12-suspended-component.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/12-suspended-component.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/12-suspended-component.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/12-suspended-component.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/12-suspended-component.jpeg"
			
			sizes="100vw"
			alt="The suspended component’s HTML and RSC Payload are streamed to the browser, as shown in the developer tools Network tab."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 12. (<a href='https://files.smashing.media/articles/forensics-react-server-components/12-suspended-component.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>We can also see that a lazy-loaded Client Component is discovered at the same time, and it contains CSS and JavaScript files that need to be fetched. These files weren’t part of the initial bundle because the component isn’t needed until later on; the code is split into their own files.</p>

<p>This way of code-splitting certainly improves the performance of the initial page load. It also makes sure that the Client Component’s code is shipped only if it’s needed. If the Server Component (which acts as the Client Component’s parent component) throws an error, then the Client Component does not load. It doesn’t make sense to load all of its code before we know whether it will load or not.</p>

<p>Figure 12 shows the <code>DOMContentLoaded</code> event is reported at the end of the page load timeline. And, just before that, we can see that the <code>localhost</code> HTTP request comes to an end. That means the server has likely sent the last zero-sized chunk, indicating to the client that the data is fully transferred and that the streaming communication can be closed.</p>

<h3 id="the-end-result">The End Result</h3>

<p>The main <code>localhost</code> HTTP request took around five seconds, but thanks to streaming, we began seeing page contents load much earlier than that. If this was a traditional SSR setup, we would likely be staring at a blank screen for those five seconds before anything arrives. On the other hand, if this was a traditional CSR setup, we would likely have shipped <em>a lot</em> more of JavaScript and put a heavy burden on both the browser and network.</p>

<p>This way, however, the app was fully interactive in those five seconds. We were able to navigate between pages and interact with Client Components that have loaded as part of the initial main bundle. This is a pure win from a user experience standpoint.</p>

<h2 id="conclusion">Conclusion</h2>

<p>RSCs mark a significant evolution in the React ecosystem. They leverage the strengths of server-side and client-side rendering while embracing HTML streaming to speed up content delivery. This approach not only addresses the SEO and loading time issues we experience with CSR but also improves SSR by reducing server load, thus enhancing performance.</p>

<p>I’ve refactored the same RSC app I shared earlier so that it uses the Next.js Page router with SSR. The improvements in RSCs are significant:</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/forensics-react-server-components/13-ssr-vs-rscs.jpeg">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/13-ssr-vs-rscs.jpeg 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/forensics-react-server-components/13-ssr-vs-rscs.jpeg 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/forensics-react-server-components/13-ssr-vs-rscs.jpeg 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/forensics-react-server-components/13-ssr-vs-rscs.jpeg 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/forensics-react-server-components/13-ssr-vs-rscs.jpeg 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/forensics-react-server-components/13-ssr-vs-rscs.jpeg"
			
			sizes="100vw"
			alt="Comparing Next.js Page Router and App Router, side-by-side."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Figure 13. (<a href='https://files.smashing.media/articles/forensics-react-server-components/13-ssr-vs-rscs.jpeg'>Large preview</a>)
    </figcaption>
  
</figure>

<p>Looking at these two reports I pulled from Sentry, we can see that streaming allows the page to start loading its resources before the actual request finishes. This significantly improves the Web Vitals metrics, which we see when comparing the two reports.</p>

<p>The conclusion: <strong>Users enjoy faster, more reactive interfaces with an architecture that relies on RSCs.</strong></p>

<p>The RSC architecture introduces two new component types: Server Components and Client Components. This division helps React and the frameworks that rely on it &mdash; like Next.js &mdash; streamline content delivery while maintaining interactivity.</p>

<p>However, this setup also introduces new challenges in areas like state management, authentication, and component architecture. Exploring those challenges is a great topic for another blog post!</p>

<p>Despite these challenges, the benefits of RSCs present a compelling case for their adoption. We definitely will see guides published on how to address RSC’s challenges as they mature, but, in my opinion, they already look like the future of rendering practices in modern web development.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Alexis Kypridemos</author><title>Converting Plain Text To Encoded HTML With Vanilla JavaScript</title><link>https://www.smashingmagazine.com/2024/04/converting-text-encoded-html-vanilla-javascript/</link><pubDate>Wed, 17 Apr 2024 13:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/04/converting-text-encoded-html-vanilla-javascript/</guid><description>What do you do when you need to convert plain text into formatted HTML? Perhaps you reach for Markdown or manually write in the element tags yourself. Or maybe you have one or two of the dozens of online tools that will do it for you. In this tutorial, Alexis Kypridemos picks those tools apart and details the steps for how we can do it ourselves with a little vanilla HTML, CSS, and JavaScript.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/04/converting-text-encoded-html-vanilla-javascript/" />
              <title>Converting Plain Text To Encoded HTML With Vanilla JavaScript</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Converting Plain Text To Encoded HTML With Vanilla JavaScript</h1>
                  
                    
                    <address>Alexis Kypridemos</address>
                  
                  <time datetime="2024-04-17T13:00:00&#43;00:00" class="op-published">2024-04-17T13:00:00+00:00</time>
                  <time datetime="2024-04-17T13:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>When copying text from a website to your device’s clipboard, there’s a good chance that you will get the formatted HTML when pasting it. Some apps and operating systems have a “Paste Special” feature that will strip those tags out for you to maintain the current style, but what do you do if that’s unavailable?</p>

<p>Same goes for converting plain text into formatted HTML. One of the closest ways we can convert plain text into HTML is writing in Markdown as an abstraction. You may have seen examples of this in many comment forms in articles just like this one. Write the comment in Markdown and it is parsed as HTML.</p>

<p>Even better would be no abstraction at all! You may have also seen (and used) a number of online tools that take plainly written text and convert it into formatted HTML. The UI makes the conversion and previews the formatted result in real time.</p>

<p>Providing a way for users to author basic web content &mdash; like comments &mdash; without knowing even the first thing about HTML, is a novel pursuit as it lowers barriers to communicating and collaborating on the web. Saying it helps “democratize” the web may be heavy-handed, but it doesn’t conflict with that vision!</p>














<figure class="
  
    break-out article__image
  
  
  ">
  
    <a href="https://files.smashing.media/articles/converting-text-encoded-html-vanilla-javascript/1-smashing-magazine-comment.png">
    
    <img
      loading="lazy"
      decoding="async"
      fetchpriority="low"
			width="800"
			height="516"
			
			srcset="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/converting-text-encoded-html-vanilla-javascript/1-smashing-magazine-comment.png 400w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_800/https://files.smashing.media/articles/converting-text-encoded-html-vanilla-javascript/1-smashing-magazine-comment.png 800w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1200/https://files.smashing.media/articles/converting-text-encoded-html-vanilla-javascript/1-smashing-magazine-comment.png 1200w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_1600/https://files.smashing.media/articles/converting-text-encoded-html-vanilla-javascript/1-smashing-magazine-comment.png 1600w,
			        https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_2000/https://files.smashing.media/articles/converting-text-encoded-html-vanilla-javascript/1-smashing-magazine-comment.png 2000w"
			src="https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_80/w_400/https://files.smashing.media/articles/converting-text-encoded-html-vanilla-javascript/1-smashing-magazine-comment.png"
			
			sizes="100vw"
			alt="Smashing Magazine comment form that is displayed at the end of articles. It says to leave a comment, followed by instructions for Markdown formatting and a form text area."
		/>
    
    </a>
  

  
    <figcaption class="op-vertical-bottom">
      Smashing Magazine’s comment form includes instructions for formatting a comment in Markdown syntax. (<a href='https://files.smashing.media/articles/converting-text-encoded-html-vanilla-javascript/1-smashing-magazine-comment.png'>Large preview</a>)
    </figcaption>
  
</figure>

<p>We can build a tool like this ourselves. I’m all for using existing resources where possible, but I’m also for demonstrating how these things work and maybe learning something new in the process.</p>

<h2 id="defining-the-scope">Defining The Scope</h2>

<p>There are plenty of assumptions and considerations that could go into a plain-text-to-HTML converter. For example, should we assume that the first line of text entered into the tool is a title that needs corresponding <code>&lt;h1&gt;</code> tags? Is each new line truly a paragraph, and how does linking content fit into this?</p>

<p>Again, the idea is that a user should be able to write without knowing Markdown or HTML syntax. This is a big constraint, and there are far too many HTML elements we might encounter, so it’s worth knowing the context in which the content is being used. For example, if this is a tool for writing blog posts, then we can limit the scope of which elements are supported based on those that are commonly used in long-form content: <code>&lt;h1&gt;</code>, <code>&lt;p&gt;</code>, <code>&lt;a&gt;</code>, and <code>&lt;img&gt;</code>. In other words, it will be possible to include top-level headings, body text, linked text, and images. There will be no support for bulleted or ordered lists, tables, or any other elements for this particular tool.</p>

<p>The front-end implementation will rely on vanilla HTML, CSS, and JavaScript to establish a small form with a simple layout and functionality that converts the text to HTML. There is a server-side aspect to this if you plan on deploying it to a production environment, but our focus is purely on the front end.</p>


<figure class="video-embed-container">
  <div class="video-embed-container--wrapper"
	
  >
    <iframe class="video-embed-container--wrapper-iframe" src="https://player.vimeo.com/video/935186868"
        frameborder="0"
        allow="autoplay; fullscreen; picture-in-picture"
        allowfullscreen>
    </iframe>
	</div>
	
</figure>

<div data-audience="non-subscriber" data-remove="true" class="feature-panel-container">

<aside class="feature-panel" style="">
<div class="feature-panel-left-col">

<div class="feature-panel-description"><p><strong>Web forms</strong> are at the center of every meaningful interaction. Meet Adam Silver&rsquo;s <strong><a href="https://www.smashingmagazine.com/printed-books/form-design-patterns/">Form Design Patterns</a></strong>, a practical guide to <strong>designing and building forms</strong> for the web.</p>
<a data-instant href="https://www.smashingmagazine.com/printed-books/form-design-patterns/" class="btn btn--green btn--large" style="">Jump to table of contents&nbsp;↬</a></div>
</div>
<div class="feature-panel-right-col"><a data-instant href="https://www.smashingmagazine.com/printed-books/form-design-patterns/" class="feature-panel-image-link">
<div class="feature-panel-image"><picture><source type="image/avif" srcSet="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/64e57b41-b7f1-4ae3-886a-806cce580ef9/form-design-patterns-shop-image-1-1.avif" />
<img
    loading="lazy"
    decoding="async"
    class="feature-panel-image-img"
    src="https://archive.smashing.media/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/51e0f837-d85d-4b28-bfab-1c9a47f0ce33/form-design-patterns-shop-image.png"
    alt="Feature Panel"
    width="481"
    height="698"
/>
</picture>
</div>
</a>
</div>
</aside>
</div>

<h2 id="looking-at-existing-solutions">Looking At Existing Solutions</h2>

<p>There are existing ways to accomplish this. For example, some libraries offer a WYSIWYG editor. Import a library like <a href="https://www.tiny.cloud/get-tiny/">TinyMCE</a> with a single <code>&lt;script&gt;</code> and you’re good to go. WYSIWYG editors are powerful and support all kinds of formatting, even applying CSS classes to content for styling.</p>

<p>But TinyMCE isn’t the most efficient package at about 500 KB minified. That’s not a criticism as much as an indication of how much functionality it covers. We want something more “barebones” than that for our simple purpose. <a href="https://github.com/search?q=txt2html&amp;type=repositories&amp;s=updated&amp;o=desc">Searching GitHub surfaces more possibilities.</a> The solutions, however, seem to fall into one of two categories:</p>

<ul>
<li>The input accepts plain text, but the generated HTML only supports the HTML <code>&lt;h1&gt;</code> and <code>&lt;p&gt;</code> tags.</li>
<li>The input converts plain text into formatted HTML, but by ”plain text,” the tool seems to mean “Markdown” (or a variety of it) instead. The <a href="https://metacpan.org/dist/txt2html/view/scripts/txt2html">txt2html Perl module</a> (from 1994!) would fall under this category.</li>
</ul>

<p>Even if a perfect solution for what we want was already out there, I’d still want to pick apart the concept of converting text to HTML to understand how it works and hopefully learn something new in the process. So, let’s proceed with our own homespun solution.</p>

<h2 id="setting-up-the-html">Setting Up The HTML</h2>

<p>We’ll start with the HTML structure for the input and output. For the input element, we’re probably best off using a <code>&lt;textarea&gt;</code>. For the output element and related styling, choices abound. The following is merely one example with some very basic CSS to place the input <code>&lt;textarea&gt;</code> on the left and an output <code>&lt;div&gt;</code> on the right:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="OJGoNOX"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Base Form Styles [forked]](https://codepen.io/smashingmag/pen/OJGoNOX) by <a href="https://codepen.io/geoffgraham">Geoff Graham</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/OJGoNOX">Base Form Styles [forked]</a> by <a href="https://codepen.io/geoffgraham">Geoff Graham</a>.</figcaption>
</figure>

<p>You can further develop the CSS, but that isn’t the focus of this article. There is no question that the design can be prettier than what I am providing here!</p>

<h2 id="capture-the-plain-text-input">Capture The Plain Text Input</h2>

<p>We’ll set an <code>onkeyup</code> event handler on the <code>&lt;textarea&gt;</code> to call a JavaScript function called <code>convert()</code> that does what it says: convert the plain text into HTML. The conversion function should accept one parameter, a string, for the user’s plain text input entered into the <code>&lt;textarea&gt;</code> element:</p>

<pre><code class="language-html">&lt;textarea onkeyup='convert(this.value);'&gt;&lt;/textarea&gt;</code></pre>

<p><code>onkeyup</code> is a better choice than <code>onkeydown</code> in this case, as <code>onkeyup</code> will call the conversion function <em>after</em> the user completes each keystroke, as opposed to <em>before</em> it happens. This way, the output, which is refreshed with each keystroke, always includes the latest typed character. If the conversion is triggered with an <code>onkeydown</code> handler, the output will exclude the most recent character the user typed. This can be frustrating when, for example, the user has finished typing a sentence but cannot yet see the final punctuation mark, say a period (<code>.</code>), in the output until typing another character first. This creates the impression of a typo, glitch, or lag when there is none.</p>

<p>In JavaScript, the <code>convert()</code> function has the following responsibilities:</p>

<ol>
<li>Encode the input in HTML.</li>
<li>Process the input line-by-line and wrap each individual line in either a <code>&lt;h1&gt;</code> or <code>&lt;p&gt;</code> HTML tag,  whichever is most appropriate.</li>
<li>Process the output of the transformations as a single string, wrap URLs in HTML <code>&lt;a&gt;</code> tags, and replace image file names with <code>&lt;img&gt;</code> elements.</li>
</ol>

<p>And from there, we display the output. We can create separate functions for each responsibility. Let’s name them accordingly:</p>

<ol>
<li><code>html_encode()</code></li>
<li><code>convert_text_to_HTML()</code></li>
<li><code>convert_images_and_links_to_HTML()</code></li>
</ol>

<p>Each function accepts one parameter, a string, and returns a string.</p>

<h2 id="encoding-the-input-into-html">Encoding The Input Into HTML</h2>

<p>Use the <code>html_encode()</code> function to HTML encode/sanitize the input. HTML encoding refers to the process of escaping or replacing certain characters in a string input to prevent users from inserting their own HTML into the output. At a minimum, we should replace the following characters:</p>

<ul>
<li><code>&lt;</code> with <code>&amp;lt;</code></li>
<li><code>&gt;</code> with <code>&amp;gt;</code></li>
<li><code>&amp;</code> with <code>&amp;amp;</code></li>
<li><code>'</code> with <code>&amp;#39;</code></li>
<li><code>&quot;</code> with <code>&amp;quot;</code></li>
</ul>

<p>JavaScript does not provide a built-in way to HTML encode input as other languages do. For example, PHP has <a href="https://www.php.net/manual/en/function.htmlspecialchars"><code>htmlspecialchars()</code></a>, <a href="https://www.php.net/manual/en/function.htmlentities"><code>htmlentities()</code></a>, and <a href="https://www.php.net/manual/en/function.strip-tags"><code>strip_tags()</code></a> functions. That said, it is relatively easy to write our own function that does this, which is what we’ll use the <code>html_encode()</code> function for that we defined earlier:</p>

<pre><code class="language-javascript">function html&#95;encode(input) {
  const textArea = document.createElement("textarea");
  textArea.innerText = input;
  return textArea.innerHTML.split("&lt;br&gt;").join("\n");
}
</code></pre>

<p>HTML encoding of the input is a critical security consideration. It prevents unwanted scripts or other HTML manipulations from getting injected into our work. Granted, front-end input sanitization and validation are both merely deterrents because bad actors can bypass them. But we may as well make them work a little harder.</p>

<p>As long as we are on the topic of securing our work, make sure to HTML-encode the input on the back end, where the user cannot interfere. At the same time, take care not to encode the input more than once. Encoding text that is already HTML-encoded will break the output functionality. The best approach for back-end storage is for the front end to pass the raw, unencoded input to the back end, then ask the back-end to HTML-encode the input before inserting it into a database.</p>

<p>That said, this only accounts for sanitizing and storing the input on the back end. We still have to display the encoded HTML output on the front end. There are at least two approaches to consider:</p>

<ol>
<li><strong>Convert the input to HTML after HTML-encoding it and before it is inserted into a database.</strong><br />
This is efficient, as the input only needs to be converted once. However, this is also an inflexible approach, as updating the HTML becomes difficult if the output requirements happen to change in the future.</li>
<li><strong>Store only the HTML-encoded input text in the database and dynamically convert it to HTML before displaying the output for each content request.</strong><br />
This is less efficient, as the conversion will occur on each request. However, it is also more flexible since it’s possible to update how the input text is converted to HTML if requirements change.</li>
</ol>

<div class="partners__lead-place"></div>

<h2 id="applying-semantic-html-tags">Applying Semantic HTML Tags</h2>

<p>Let’s use the <code>convert_text_to_HTML()</code> function we defined earlier to wrap each line in their respective HTML tags, which are going to be either <code>&lt;h1&gt;</code> or <code>&lt;p&gt;</code>. To determine which tag to use, we will <code>split</code> the text input on the newline character (<code>\n</code>) so that the text is processed as an array of lines rather than a single string, allowing us to evaluate them individually.</p>

<div class="break-out">
<pre><code class="language-javascript">function convert&#95;text&#95;to&#95;HTML(txt) {
  // Output variable
  let out = '';
  // Split text at the newline character into an array
  const txt&#95;array = txt.split("\n");
  // Get the number of lines in the array
  const txt&#95;array&#95;length = txt&#95;array.length;
  // Variable to keep track of the (non-blank) line number
  let non&#95;blank&#95;line&#95;count = 0;
  
  for (let i = 0; i &lt; txt&#95;array&#95;length; i++) {
    // Get the current line
    const line = txt&#95;array[i];
    // Continue if a line contains no text characters
    if (line === ''){
      continue;
    }
    
    non&#95;blank&#95;line&#95;count++;
    // If a line is the first line that contains text
    if (non&#95;blank&#95;line&#95;count === 1){
      // ...wrap the line of text in a Heading 1 tag
      out += `&lt;h1&gt;${line}&lt;/h1&gt;`;
      // ...otherwise, wrap the line of text in a Paragraph tag.
    } else {
      out += `&lt;p&gt;${line}&lt;/p&gt;`;
    }
  }

  return out;
}
</code></pre>
</div>

<p>In short, this little snippet loops through the array of split text lines and ignores lines that do <em>not</em> contain any text characters. From there, we can evaluate whether a line is the first one in the series. If it is, we slap a <code>&lt;h1&gt;</code> tag on it; otherwise, we mark it up in a <code>&lt;p&gt;</code> tag.</p>

<p>This logic could be used to account for other types of elements that you may want to include in the output. For example, perhaps the second line is assumed to be a byline that names the author and links up to an archive of all author posts.</p>

<h2 id="tagging-urls-and-images-with-regular-expressions">Tagging URLs And Images With Regular Expressions</h2>

<p>Next, we’re going to create our <code>convert_images_and_links_to_HTML()</code> function to encode URLs and images as HTML elements. It’s a good chunk of code, so I’ll drop it in and we’ll immediately start picking it apart together to explain how it all works.</p>

<div class="break-out">
<pre><code class="language-javascript">
function convert&#95;images&#95;and&#95;links&#95;to&#95;HTML(string){
  let urls&#95;unique = [];
  let images&#95;unique = [];
  const urls = string.match(/https&#42;:\/\/[^\s&lt;),]+[^\s&lt;),.]/gmi) ?? [];
  const imgs = string.match(/[^"'&gt;\s]+\.(jpg|jpeg|gif|png|webp)/gmi) ?? [];
                          
  const urls&#95;length = urls.length;
  const images&#95;length = imgs.length;
  
  for (let i = 0; i &lt; urls&#95;length; i++){
    const url = urls[i];
    if (!urls&#95;unique.includes(url)){
      urls&#95;unique.push(url);
    }
  }
  
  for (let i = 0; i &lt; images&#95;length; i++){
    const img = imgs[i];
    if (!images&#95;unique.includes(img)){
      images&#95;unique.push(img);
    }
  }
  
  const urls&#95;unique&#95;length = urls&#95;unique.length;
  const images&#95;unique&#95;length = images&#95;unique.length;
  
  for (let i = 0; i &lt; urls&#95;unique&#95;length; i++){
    const url = urls&#95;unique[i];
    if (images&#95;unique&#95;length === 0 || !images&#95;unique.includes(url)){
      const a&#95;tag = `&lt;a href="${url}" target="&#95;blank"&gt;${url}&lt;/a&gt;`;
      string = string.replace(url, a&#95;tag);
    }
  }
  
  for (let i = 0; i &lt; images&#95;unique&#95;length; i++){
    const img = images&#95;unique[i];
    const img&#95;tag = `&lt;img src="${img}" alt=""&gt;`;
    const img&#95;link = `&lt;a href="${img}"&gt;${img&#95;tag}&lt;/a&gt;`;
    string = string.replace(img, img&#95;link);
  }
  return string;
}
</code></pre>
</div>

<p>Unlike the <code>convert_text_to_HTML()</code> function, here we use regular expressions to identify the terms that need to be wrapped and/or replaced with <code>&lt;a&gt;</code> or <code>&lt;img&gt;</code> tags. We do this for a couple of reasons:</p>

<ol>
<li>The previous <code>convert_text_to_HTML()</code> function handles text that would be transformed to the HTML block-level elements <code>&lt;h1&gt;</code> and <code>&lt;p&gt;</code>, and, if you want, other block-level elements such as <code>&lt;address&gt;</code>. Block-level elements in the HTML output correspond to discrete lines of text in the input, which you can think of as paragraphs, the text entered between presses of the <kbd>Enter</kbd> key.</li>
<li>On the other hand, URLs in the text input are often included in the middle of a sentence rather than on a separate line. Images that occur in the input text are often included on a separate line, but not always. While you could identify text that represents URLs and images by processing the input line-by-line &mdash; or even word-by-word, if necessary &mdash; it is easier to use regular expressions and process the entire input as a single string rather than by individual lines.</li>
</ol>

<p>Regular expressions, though they are powerful and the appropriate tool to use for this job, <a href="https://blog.bitsrc.io/threats-of-using-regular-expressions-in-javascript-28ddccf5224c">come with a performance cost</a>, which is another reason to use each expression only once for the entire text input.</p>

<p><strong>Remember</strong>: <em>All the JavaScript in this example runs each time the user types a character, so it is important to keep things as lightweight and efficient as possible.</em></p>

<p>I also want to make a note about the variable names in our <code>convert_images_and_links_to_HTML()</code> function. <code>images</code> (plural), <code>image</code> (singular), and <code>link</code> are reserved words in JavaScript. Consequently, <code>imgs</code>, <code>img</code>, and <code>a_tag</code> were used for naming. Interestingly, these specific reserved words are not listed on the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words">relevant MDN page</a>, but they are on <a href="https://www.w3schools.com/js/js_reserved.asp">W3Schools</a>.</p>

<p>We’re using the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match"><code>String.prototype.match()</code></a> function for each of the two regular expressions, then storing the results for each call in an array. From there, we use the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing">nullish coalescing operator</a> (<code>??</code>) on each call so that, if no matches are found, the result will be an empty array. If we do not do this and no matches are found, the result of each <code>match()</code> call will be <code>null</code> and will cause problems downstream.</p>

<div class="break-out">
<pre><code class="language-javascript">const urls = string.match(/https&#42;:\/\/[^\s&lt;),]+[^\s&lt;),.]/gmi) ?? [];
const imgs = string.match(/[^"'&gt;\s]+\.(jpg|jpeg|gif|png|webp)/gmi) ?? [];
</code></pre>
</div>

<p>Next up, we filter the arrays of results so that <strong>each array contains only unique results</strong>. This is a critical step. If we don’t filter out duplicate results and the input text contains multiple instances of the same URL or image file name, then we break the HTML tags in the output. JavaScript does not provide a simple, built-in method to get unique items in an array that’s akin to the PHP <a href="https://www.php.net/manual/en/function.array-unique"><code>array_unique()</code></a> function.</p>

<p>The code snippet works around this limitation using an admittedly ugly but straightforward procedural approach. The same problem is solved using a more functional approach if you prefer. There are many articles on the web describing <a href="https://www.geeksforgeeks.org/how-to-get-all-unique-values-remove-duplicates-in-a-javascript-array/">various ways to filter a JavaScript array</a> in order to <a href="https://chrisengelsma.medium.com/filtering-an-array-to-get-only-unique-values-in-javascript-and-typescript-3342e47b9448">keep only the unique items</a>.</p>

<p>We’re also checking if the URL is matched as an image before replacing a URL with an appropriate <code>&lt;a&gt;</code> tag and performing the replacement only if the URL doesn’t match an image. We may be able to avoid having to perform this check by using a more intricate regular expression. The example code deliberately uses regular expressions that are perhaps less precise but hopefully easier to understand in an effort to keep things as simple as possible.</p>

<p>And, finally, we’re replacing image file names in the input text with <code>&lt;img&gt;</code> tags that have the <code>src</code> attribute set to the image file name. For example, <code>my_image.png</code> in the input is transformed into <code>&lt;img src='my_image.png'&gt;</code> in the output. We wrap each <code>&lt;img&gt;</code> tag with an <code>&lt;a&gt;</code> tag that links to the image file and opens it in a new tab when clicked.</p>

<p>There are a couple of benefits to this approach:</p>

<ul>
<li>In a real-world scenario, you will likely use a CSS rule to constrain the size of the rendered image. By making the images clickable, you provide users with a convenient <strong>way to view the full-size image</strong>.</li>
<li>If the image is not a local file but is instead a URL to an image from a third party, this is a <strong>way to implicitly provide attribution</strong>. Ideally, you should not rely solely on this method but, instead, provide explicit attribution underneath the image in a <code>&lt;figcaption&gt;</code>, <code>&lt;cite&gt;</code>, or similar element. But if, for whatever reason, you are unable to provide explicit attribution, you are at least providing a link to the image source.</li>
</ul>

<p>It may go without saying, but <a href="https://www.pixsy.com/image-protection/hotlinking">“hotlinking”</a> images is something to avoid. Use only locally hosted images wherever possible, and provide attribution if you do not hold the copyright for them.</p>

<p>Before we move on to displaying the converted output, let’s talk a bit about accessibility, specifically the image <code>alt</code> attribute. The example code I provided does add an <code>alt</code> attribute in the conversion but does not populate it with a value, as there is no easy way to automatically calculate what that value should be. An empty <code>alt</code> attribute can be acceptable if the image is considered “decorative,” i.e., purely supplementary to the surrounding text. But one may argue that <a href="https://www.smashingmagazine.com/2021/06/img-alt-attribute-alternate-description-decorative/">there is no such thing as a purely decorative image</a>.</p>

<p>That said, I consider this to be a limitation of what we’re building.</p>

<div class="partners__lead-place"></div>

<h2 id="displaying-the-output-html">Displaying the Output HTML</h2>

<p>We’re at the point where we can finally work on displaying the HTML-encoded output! We&rsquo;ve already handled all the work of converting the text, so all we really need to do now is call it:</p>

<div class="break-out">
<pre><code class="language-javascript">function convert(input&#95;string) {
  output.innerHTML = convert&#95;images&#95;and&#95;links&#95;to&#95;HTML(convert&#95;text&#95;to&#95;HTML(html&#95;encode(input&#95;string)));
}
</code></pre>
</div>

<p>If you would rather display the output string as raw HTML markup, use a <code>&lt;pre&gt;</code> tag as the output element instead of a <code>&lt;div&gt;</code>:</p>

<pre><code class="language-javascript">&lt;pre id='output'&gt;&lt;/pre&gt;
</code></pre>

<p>The only thing to note about this approach is that you would target the <code>&lt;pre&gt;</code> element’s <code>textContent</code> instead of <code>innerHTML</code>:</p>

<div class="break-out">
<pre><code class="language-javascript">function convert(input&#95;string) {
  output.textContent = convert&#95;images&#95;and&#95;links&#95;to&#95;HTML(convert&#95;text&#95;to&#95;HTML(html&#95;encode(input&#95;string)));
}
</code></pre>
</div>

<h2 id="conclusion">Conclusion</h2>

<p>We did it! We built one of the same sort of copy-paste tool that converts plain text on the spot. In this case, we’ve configured it so that plain text entered into a <code>&lt;textarea&gt;</code> is parsed line-by-line and encoded into HTML that we format and display inside another element.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="yLrxOzP"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [Convert Plain Text to HTML (PoC) [forked]](https://codepen.io/smashingmag/pen/yLrxOzP) by <a href="https://codepen.io/geoffgraham">Geoff Graham</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/yLrxOzP">Convert Plain Text to HTML (PoC) [forked]</a> by <a href="https://codepen.io/geoffgraham">Geoff Graham</a>.</figcaption>
</figure>

<p>We were even able to keep the solution fairly simple, i.e., vanilla HTML, CSS, and JavaScript, without reaching for a third-party library or framework. Does this simple solution do everything a ready-made tool like a framework can do? Absolutely not. But a solution as simple as this is often all you need: nothing more and nothing less.</p>

<p>As far as scaling this further, the code could be modified to <code>POST</code> what’s entered into the <code>&lt;form&gt;</code> using a PHP script or the like. That would be a great exercise, and if you do it, please share your work with me in the comments because I’d love to check it out.</p>

<h3 id="references">References</h3>

<ul>
<li>“<a href="https://www.w3docs.com/snippets/javascript/how-to-html-encode-a-string.html">How to HTML-encode a String</a>” (W3Docs)</li>
<li>“<a href="https://www.educative.io/answers/how-to-escape-unescape-html-characters-in-string-in-javascript">How to escape &amp; unescape HTML characters in string in JavaScript</a>” (Educative.io)</li>
<li>“<a href="https://www.geeksforgeeks.org/how-to-get-all-unique-values-remove-duplicates-in-a-javascript-array/">How to get all unique values (remove duplicates) in a JavaScript array?”</a>” (GeeksforGeeks)</li>
<li>“<a href="https://chrisengelsma.medium.com/filtering-an-array-to-get-only-unique-values-in-javascript-and-typescript-3342e47b9448">Getting Unique Array Values in Javascript and Typescript</a>,” Chris Engelsma</li>
<li>“<a href="https://blog.bitsrc.io/threats-of-using-regular-expressions-in-javascript-28ddccf5224c">Threats of Using Regular Expressions in JavaScript</a>,” Dulanka Karunasena</li>
</ul>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item><item><author>Henry Bley-Vroman</author><title>Setting And Persisting Color Scheme Preferences With CSS And A “Touch” Of JavaScript</title><link>https://www.smashingmagazine.com/2024/03/setting-persisting-color-scheme-preferences-css-javascript/</link><pubDate>Mon, 25 Mar 2024 12:00:00 +0000</pubDate><guid>https://www.smashingmagazine.com/2024/03/setting-persisting-color-scheme-preferences-css-javascript/</guid><description>There are many ways to approach a “Dark Mode” feature that respects a user’s system color scheme preferences and allows for per-site customization. With recent developments in CSS, core functionality that has always required JavaScript can now be done without it. Henry Bley-Vroman walks through a new possibility that leans into cutting-edge CSS, with minimal JavaScript to support persisting the user’s color scheme preference across pages.</description><content:encoded><![CDATA[
          <html>
            <head>
              <meta charset="utf-8">
              <link rel="canonical" href="https://www.smashingmagazine.com/2024/03/setting-persisting-color-scheme-preferences-css-javascript/" />
              <title>Setting And Persisting Color Scheme Preferences With CSS And A “Touch” Of JavaScript</title>
            </head>
            <body>
              <article>
                <header>
                  <h1>Setting And Persisting Color Scheme Preferences With CSS And A “Touch” Of JavaScript</h1>
                  
                    
                    <address>Henry Bley-Vroman</address>
                  
                  <time datetime="2024-03-25T12:00:00&#43;00:00" class="op-published">2024-03-25T12:00:00+00:00</time>
                  <time datetime="2024-03-25T12:00:00&#43;00:00" class="op-modified">2026-02-09T03:03:08+00:00</time>
                </header>
                
                

<p>Many modern websites give users the power to set a site-specific color scheme preference. A basic implementation is straightforward with JavaScript: listen for when a user changes a checkbox or clicks a button, toggle a class (or attribute) on the <code>&lt;body&gt;</code> element in response, and write the styles for that class to override design with a different color scheme.</p>

<p>CSS’s new <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:has"><code>:has()</code> pseudo-class</a>, supported by major browsers since December 2023, opens many doors for front-end developers. I’m especially excited about leveraging it to modify UI in response to user interaction <em>without JavaScript</em>. Where previously we have used JavaScript to toggle classes or attributes (or to set styles directly), we can now pair <code>:has()</code> selectors with HTML’s native interactive elements.</p>

<p>Supporting a color scheme preference, like “Dark Mode,” is a great use case. We can use a <code>&lt;select&gt;</code> element anywhere that toggles color schemes based on the selected <code>&lt;option&gt;</code> &mdash; no JavaScript needed, save for a sprinkle to save the user’s choice, which we’ll get to further in.</p>

<h2 id="respecting-system-preferences">Respecting System Preferences</h2>

<p>First, we’ll support a user’s system-wide color scheme preferences by adopting a “Light Mode”-first approach. In other words, we start with a light color scheme by default and swap it out for a dark color scheme for users who prefer it.</p>

<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme"><code>prefers-color-scheme</code></a> media feature detects the user’s system preference. Wrap “dark” styles in a <code>prefers-color-scheme: dark</code> media query.</p>

<pre><code class="language-css">selector {
  /&#42; light styles &#42;/

  @media (prefers-color-scheme: dark) {
    /&#42; dark styles &#42;/
  }
}
</code></pre>

<p>Next, set the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme"><code>color-scheme</code></a> property to match the preferred color scheme. Setting <code>color-scheme: dark</code> switches the browser into its built-in dark mode, which includes a black default background, white default text, “dark” styles for scrollbars, and other elements that are difficult to target with CSS, and more. I’m using CSS variables to hint that the value is dynamic &mdash; and because I like the browser developer tools experience &mdash; but plain <code>color-scheme: light</code> and <code>color-scheme: dark</code> would work fine.</p>

<pre><code class="language-css">:root {
  /&#42; light styles here &#42;/
  color-scheme: var(--color-scheme, light);
  
  /&#42; system preference is "dark" &#42;/
  @media (prefers-color-scheme: dark) {
    --color-scheme: dark;
    /&#42; any additional dark styles here &#42;/
  }
}
</code></pre>

<h2 id="giving-users-control">Giving Users Control</h2>

<p>Now, to support <em>overriding</em> the system preference, let users choose between light (default) and dark color schemes at the page level.</p>

<p>HTML has native elements for handling user interactions. Using one of those controls, rather than, say, a <code>&lt;div&gt;</code> nest, improves the chances that assistive tech users will have a good experience. I’ll use a <code>&lt;select&gt;</code> menu with options for “system,” “light,” and “dark.” A group of <code>&lt;input type=&quot;radio&quot;&gt;</code> would work, too, if you wanted the options right on the surface instead of a dropdown menu.</p>

<pre><code class="language-html">&lt;select id="color-scheme"&gt;
  &lt;option value="system" selected&gt;System&lt;/option&gt;
  &lt;option value="light"&gt;Light&lt;/option&gt;
  &lt;option value="dark"&gt;Dark&lt;/option&gt;
&lt;/select&gt;
</code></pre>

<p>Before CSS gained <code>:has()</code>, responding to the user’s selected <code>&lt;option&gt;</code> required JavaScript, for example, setting an event listener on the <code>&lt;select&gt;</code> to toggle a class or attribute on <code>&lt;html&gt;</code> or <code>&lt;body&gt;</code>.</p>

<p>But now that we have <code>:has()</code>, we can now do this with CSS alone! You’ll save spending any of your performance budget on a dark mode script, plus the control will work even for users who have disabled JavaScript. And any “no-JS” folks on the project will be satisfied.</p>

<p>What we need is a selector that applies to the page when it <code>:has()</code> a <code>select</code> menu with a particular <code>[value]:checked</code>. Let’s translate that into CSS:</p>

<pre><code class="language-css">:root:has(select option[value="dark"]:checked)</code></pre>

<p>We’re defaulting to a light color scheme, so it’s enough to account for two possible dark color scheme scenarios:</p>

<ol>
<li>The page-level color preference is “system,” and the system-level preference is “dark.”</li>
<li>The page-level color preference is “dark”.</li>
</ol>

<p>The first one is a page-preference-aware iteration of our <code>prefers-color-scheme: dark</code> case. A “dark” system-level preference is no longer enough to warrant dark styles; we need a “dark” system-level preference and a “follow the system-level preference” at the page-level preference. We’ll wrap the <code>prefers-color-scheme</code> media query dark scheme styles with the <code>:has()</code> selector we just wrote:</p>

<div class="break-out">
<pre><code class="language-css">:root {
  /&#42; light styles here &#42;/
  color-scheme: var(--color-scheme, light);
    
  /&#42; page preference is "system", and system preference is "dark" &#42;/
  @media (prefers-color-scheme: dark) {
    &:has(#color-scheme option[value="system"]:checked) {
      --color-scheme: dark;
      /&#42; any additional dark styles, again &#42;/
    }
  }
}
</code></pre>
</div>

<p>Notice that I’m using <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector">CSS Nesting</a> in that last snippet. <a href="https://github.com/web-platform-dx/web-features/blob/main/docs/baseline.md">Baseline 2023</a> has it pegged as “Newly available across major browsers” which means support is good, but at the time of writing, support on Android browsers not included in <a href="https://github.com/web-platform-dx/web-features/blob/main/docs/baseline.md#core-browser-set">Baseline’s core browser set</a> is <a href="https://caniuse.com/css-nesting">limited</a>. You can get the same result without nesting.</p>

<div class="break-out">
<pre><code class="language-css">:root {
  /&#42; light styles &#42;/
  color-scheme: var(--color-scheme, light);
    
  /&#42; page preference is "dark" &#42;/
  &:has(#color-scheme option[value="dark"]:checked) {
    --color-scheme: dark;
    /&#42; any additional dark styles &#42;/
  }
}
</code></pre>
</div>

<p>For the second dark mode scenario, we’ll use nearly the exact same <code>:has()</code> selector as we did for the first scenario, this time checking whether the “dark” option &mdash; rather than the “system” option &mdash; is selected:</p>

<div class="break-out">
<pre><code class="language-css">:root {
  /&#42; light styles &#42;/
  color-scheme: var(--color-scheme, light);
    
  /&#42; page preference is "dark" &#42;/
  &:has(#color-scheme option[value="dark"]:checked) {
    --color-scheme: dark;
    /&#42; any additional dark styles &#42;/
  }
    
  /&#42; page preference is "system", and system preference is "dark" &#42;/
  @media (prefers-color-scheme: dark) {
    &:has(#color-scheme option[value="system"]:checked) {
      --color-scheme: dark;
      /&#42; any additional dark styles, again &#42;/
    }
  }
}
</code></pre>
</div>

<p>Now the page’s styles respond to both changes in users’ system settings <em>and</em> user interaction with the page’s color preference UI &mdash; all with CSS!</p>

<p>But the colors change <em>instantly</em>. Let’s smooth the transition.</p>

<h2 id="respecting-motion-preferences">Respecting Motion Preferences</h2>

<p>Instantaneous style changes can feel inelegant in some cases, and this is one of them. So, let’s apply a CSS transition on the <code>:root</code> to “ease” the switch between color schemes. (Transition styles at the <code>:root</code> will cascade down to the rest of the page, which may necessitate adding <code>transition: none</code> or other transition overrides.)</p>

<p>Note that the CSS <code>color-scheme</code> property does not support transitions.</p>

<div class="break-out">
<pre><code class="language-css">:root {
  transition-duration: 200ms;
  transition-property: /&#42; properties changed by your light/dark styles &#42;/;
}
</code></pre>
</div>

<p>Not all users will consider the addition of a transition a welcome improvement. Querying the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion"><code>prefers-reduced-motion</code></a> media feature allows us to account for a user’s motion preferences. If the value is set to <code>reduce</code>, then we remove the <code>transition-duration</code> to eliminate unwanted motion.</p>

<div class="break-out">
<pre><code class="language-css">:root {
  transition-duration: 200ms;
  transition-property: /&#42; properties changed by your light/dark styles &#42;/;
    
  @media screen and (prefers-reduced-motion: reduce) {
    transition-duration: none;
  }
}
</code></pre>
</div>

<p>Transitions can also produce poor user experiences on devices that render changes slowly, for example, ones with e-ink screens. We can extend our “no motion condition” media query to account for that with the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/update"><code>update</code></a> media feature. If its value is <code>slow</code>, then we remove the <code>transition-duration</code>.</p>

<div class="break-out">
<pre><code class="language-css">:root {
  transition-duration: 200ms;
  transition-property: /&#42; properties changed by your light/dark styles &#42;/;
    
  @media screen and (prefers-reduced-motion: reduce), (update: slow) {
    transition-duration: 0s;
  }
}
</code></pre>
</div>

<p>Let’s try out what we have so far in the following demo. Notice that, to work around <code>color-scheme</code>’s lack of transition support, I’ve explicitly styled the properties that should transition during theme changes.</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="YzMVQja"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS-only theme switcher (requires :has()) [forked]](https://codepen.io/smashingmag/pen/YzMVQja) by <a href="https://codepen.io/henry">Henry</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/YzMVQja">CSS-only theme switcher (requires :has()) [forked]</a> by <a href="https://codepen.io/henry">Henry</a>.</figcaption>
</figure>

<p>Not bad! But what happens if the user refreshes the pages or navigates to another page? The reload effectively wipes out the user’s form selection, forcing the user to re-make the selection. That may be acceptable in some contexts, but it’s likely to go against user expectations. Let’s bring in JavaScript for a touch of progressive enhancement in the form of…</p>

<h2 id="persistence">Persistence</h2>

<p>Here’s a vanilla JavaScript implementation. It’s a naive starting point &mdash; the functions and variables aren’t encapsulated but are instead properties on <code>window</code>. You’ll want to adapt this in a way that fits your site’s conventions, framework, library, and so on.</p>

<p>When the user changes the color scheme from the <code>&lt;select&gt;</code> menu, we’ll store the selected <code>&lt;option&gt;</code> value in a new <code>localStorage</code> item called <code>&quot;preferredColorScheme&quot;</code>. On subsequent page loads, we’ll check <code>localStorage</code> for the <code>&quot;preferredColorScheme&quot;</code> item. If it exists, and if its value corresponds to one of the form control options, we restore the user’s preference by programmatically updating the menu selection.</p>

<div class="break-out">
<pre><code class="language-javascript">/&#42;
 &#42; If a color scheme preference was previously stored,
 &#42; select the corresponding option in the color scheme preference UI
 &#42; unless it is already selected.
 &#42;/
function restoreColorSchemePreference() {
  const colorScheme = localStorage.getItem(colorSchemeStorageItemName);

  if (!colorScheme) {
    // There is no stored preference to restore
    return;
  }

  const option = colorSchemeSelectorEl.querySelector(`[value=${colorScheme}]`);  

  if (!option) {
    // The stored preference has no corresponding option in the UI.
    localStorage.removeItem(colorSchemeStorageItemName);
    return;
  }

  if (option.selected) {  
    // The stored preference's corresponding menu option is already selected
    return;
  }

  option.selected = true;
}

/&#42;
 &#42; Store an event target's value in localStorage under colorSchemeStorageItemName
 &#42;/
function storeColorSchemePreference({ target }) {
  const colorScheme = target.querySelector(":checked").value;
  localStorage.setItem(colorSchemeStorageItemName, colorScheme);
}

// The name under which the user's color scheme preference will be stored.
const colorSchemeStorageItemName = "preferredColorScheme";

// The color scheme preference front-end UI.
const colorSchemeSelectorEl = document.querySelector("#color-scheme");

if (colorSchemeSelectorEl) {
  restoreColorSchemePreference();

  // When the user changes their color scheme preference via the UI,
  // store the new preference.
  colorSchemeSelectorEl.addEventListener("input", storeColorSchemePreference);
}
</code></pre>
</div>

<p>Let’s try that out. Open this demo (perhaps in a new window), use the menu to change the color scheme, and then refresh the page to see your preference persist:</p>

<figure class="break-out">
	<p data-height="480"
	data-theme-id="light"
	data-slug-hash="GRLmEXX"
	data-user="smashingmag"
	data-default-tab="result"
	class="codepen">See the Pen [CSS-only theme switcher (requires :has()) with JS persistence [forked]](https://codepen.io/smashingmag/pen/GRLmEXX) by <a href="https://codepen.io/henry">Henry</a>.</p>
	<figcaption>See the Pen <a href="https://codepen.io/smashingmag/pen/GRLmEXX">CSS-only theme switcher (requires :has()) with JS persistence [forked]</a> by <a href="https://codepen.io/henry">Henry</a>.</figcaption>
</figure>

<p>If your system color scheme preference is “light” and you set the demo’s color scheme to “dark,” you may get the light mode styles for a moment immediately after reloading the page before the dark mode styles kick in. That’s because CodePen loads its own JavaScript before the demo’s scripts. That is out of my control, but you can take care to improve this persistence on your projects.</p>

<h2 id="persistence-performance-considerations">Persistence Performance Considerations</h2>

<p>Where things can get tricky is restoring the user’s preference <em>immediately</em> after the page loads. If the color scheme preference in <code>localStorage</code> is different from the user’s system-level color scheme preference, it’s possible the user will see the system preference color scheme before the page-level preference is restored. (Users who have selected the “System” option will never get that flash; neither will those whose system settings match their selected option in the form control.)</p>

<p>If your implementation is showing a <a href="https://css-tricks.com/flash-of-inaccurate-color-theme-fart/">“flash of inaccurate color theme”</a>, where is the problem happening? Generally speaking, the earlier the scripts appear on the page, the lower the risk. The “best option” for you will depend on your specific stack, of course.</p>

<h2 id="what-about-browsers-that-don-t-support-has">What About Browsers That Don’t Support <code>:has()</code>?</h2>

<p><a href="https://caniuse.com/css-has">All major browsers support <code>:has()</code> today</a> Lean into modern platforms if you can. But if you do need to consider legacy browsers, like Internet Explorer, there are two directions you can go: either hide or remove the color scheme picker for those browsers or make heavier use of JavaScript.</p>

<p>If you consider color scheme support itself a progressive enhancement, you can entirely hide the selection UI in browsers that don’t support <code>:has()</code>:</p>

<pre><code class="language-css">@supports not selector(:has(body)) {
  @media (prefers-color-scheme: dark) {
    :root {
      /&#42; dark styles here &#42;/
    }
  }

  #color-scheme {
    display: none;
  }
}
</code></pre>

<p>Otherwise, you’ll need to rely on a JavaScript solution not only for persistence but for the core functionality. Go back to that traditional event listener toggling a class or attribute.</p>

<p>The CSS-Tricks “<a href="https://css-tricks.com/a-complete-guide-to-dark-mode-on-the-web/">Complete Guide to Dark Mode</a>” details several alternative approaches that you might consider as well when working on the legacy side of things.</p>

<div class="signature">
  <img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial" width="35" height="46" loading="lazy" decoding="async" />
  <span>(gg, yk)</span>
</div>


              </article>
            </body>
          </html>
        ]]></content:encoded></item></channel></rss>