<?xml version="1.0" encoding="utf-8"?>
  <rss version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:wfw="http://wellformedweb.org/CommentAPI/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:atom="http://www.w3.org/2005/Atom"
    xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
    xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
    xmlns:georss="http://www.georss.org/georss"
    xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
  >
    <channel>
      <title>Piccalilli - Progressive Enhancement topic archive</title>
      <link>https://piccalil.li/</link>
      <atom:link href="https://piccalil.li/category/progressive-enhancement.xml" rel="self" type="application/rss+xml" />
      <description>We are Piccalilli. A publication dedicated to providing high quality educational content to level up your front-end skills.</description>
      <language>en-GB</language>
      <copyright>Piccalilli - Progressive Enhancement topic archive 2026</copyright>
      <docs>https://www.rssboard.org/rss-specification</docs>
      <pubDate>Mon, 06 Apr 2026 16:02:07 GMT</pubDate>
      <lastBuildDate>Mon, 06 Apr 2026 16:02:07 GMT</lastBuildDate>

      
      <item>
        <title>It’s about time I tried to explain what progressive enhancement actually is</title>
        <link>https://piccalil.li/blog/its-about-time-i-tried-to-explain-what-progressive-enhancement-actually-is/?ref=progressive-enhancement-category-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Wed, 03 Jul 2024 12:56:24 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/its-about-time-i-tried-to-explain-what-progressive-enhancement-actually-is/?ref=progressive-enhancement-category-rss-feed</guid>
        <description><![CDATA[<p>I’m a long-time proponent of progressive enhancement to the point where readers are almost certainly bored, but I am like I am because <strong>it’s important</strong>. I’ve been saying the following for years:</p>
<blockquote>
<p><strong>We build for everyone</strong>. Not just for ourselves or our peer groups.</p>
</blockquote>
<p>Do you want to build resilient front-ends? Do you want your front-end to work for everyone, regardless of browser and device? Do you want your website to work on poor connections? Do you want your website to work globally?</p>
<p>Have you answered yes to either or all of the above? What you need is <strong>progressive enhancement</strong>.</p>
<h2>What is progressive enhancement?</h2>
<p>Let’s go for a real simple definition here.</p>
<p>Progressive enhancement is a design and development principle where we build in layers which automatically turn themselves on based on the browser’s capabilities. Enhancement layers are treated as off by default, resulting in a solid baseline experience that is designed to work for everyone.</p>
<p>We do this with a declarative approach which is already baked in to how the browser deals with HTML and CSS. For JavaScript — which is imperative — we only use it as an experience enhancer, rather than a requirement, which means only loading when the core elements of the page — HTML and CSS — are already providing a great user experience.</p>
<p>The idea of progressive enhancement is that <strong>everyone gets the perfect experience for them</strong>, rather than a pre-determined “perfect” experience from a design and development team. It’s a perfect experience for users because everything works, which is far from the truth — in reality — with so-called “graceful degradation”.</p>
<p>Graceful degradation is the opposite to progressive enhancement because a pre-determined “perfect experience” is authored then broken down with support testing strategies instead. The problem with that approach is <strong>everything can and will go wrong in the browser</strong>, so support testing strategies will unlikely never be reached in <strong>a lot of cases</strong>, which results in a broken, unresponsive user interface.</p>
<p>It’s why at the root of progressive enhancement is solid, organised and semantic HTML. If the absolute worst should happen with everything else on your webpage (it will, trust me), then at <em>least</em> the user gets a functional, understandable web page.</p>
<p>Above everything, it is better to presume the worst when building a website because when someone actually experiences the worst, they still get a good experience.</p>
<h2>It’s not just an anti-JavaScript thing, it’s a mental model rooted in iteration</h2>
<p>Building with progressive enhancement principles isn’t anti-JavaScript, but rather, it <em>rightly</em> places JavaScript as a nice to have, instead of being a required technology. JavaScript is a fantastic tool to provide rich interactivity to a page, but it is — by its imperative nature — <em>fragile</em>. It’s better to enhance existing correctly working functionality than bet against that functionality working in the real world by making an imperative programming language that’s notorious for behaving poorly on the web the barrier to it.</p>
<p>The same goes for new CSS features. We can operate with the latest and greatest features without fear because if a browser doesn’t understand them, it’s all good because we’re building up from a <strong>minimum viable experience</strong>. Luckily CSS is a declarative programming language, so we can determine what our default and enhanced experiences are easily in the same rule a lot of the time.</p>
<p>Let’s say you want to use the new(ish) <code>cap</code> unit. <a href="https://piccalil.li/blog/a-primer-on-the-cascade-and-specificity">Allow the cascade to solve the problem for you</a> by setting the <code>cap</code> value <em>after</em> a sensible default:</p>
<pre><code>.my-element {
	height: 1.5em;
	height: 1cap;
}
</code></pre>
<p>When it comes to more complex CSS, break the design down into an acceptable, functional default then build up to the enhanced version — always testing in various browser and device configurations to make sure the minimum viable experience is working as expected with each iteration.</p>
<h2>What is a minimum viable experience?</h2>
<p>Essentially, provide the most possible value to a user with the least amount of technical capability. It’s all about the <a href="https://adactio.com/journal/14327">rule of least power</a>.</p>
<p>I think the best way to describe that is <a href="https://piccalil.li/blog/how-a-minimum-viable-experience-produces-a-resilient-inclusive-end-product">link up to a post I wrote last year</a> and pick out this part:</p>
<blockquote>
<p>A good example of a minimum viable experience is an email client that is just a HTML form that when submitted, sends an email to the specified address. You could then enhance this minimum viable experience if JavaScript is available by adding client-side validation, auto-complete and all of the other handy tricks our modern email clients give us. The important thing is that when JavaScript fails, we can still provide the basic capability that the user needs.</p>
</blockquote>
<h2>Almost no one will get your “ideal” experience, especially not all the time</h2>
<p>I’m writing this article on a train to London, tethering on my phone. Currently my connection speed is just shy of 2mbps and constantly cuts out. What happens if I load a heavy website, built to be the “perfect” experience? I get a blank white screen and a console full of errors.</p>
<p>That’s just me right now, but <strong>life happens</strong> and people are never in the perfect state you might imagine them to be during production. <a href="https://andy-bell.co.uk/this-is-why-performance-matters/">Here’s an example of how finding information on a power cut was next to impossible</a> because the <em>extremely heavy</em> user interface couldn’t load on a dodgy mobile connection. Always remember that <a href="https://en.wikipedia.org/wiki/List_of_sovereign_states_by_Internet_connection_speeds#Mobile_connection">the quality of internet connections worldwide varies, massively</a> .</p>
<p>Instead of building to make you and the stakeholders happy at the point of your project timeline: make everyone happy by building with progressive enhancement instead because more money — by proxy of more successful user sessions — sure makes stakeholders happy.</p>
<h2>“Designers won’t work like this”</h2>
<p>Have you communicated that with them, or are you making an assumption?</p>
<p>Honestly, in the <a href="https://piccalil.li/author/andy-bell"><em>years</em> that I have helped organisations with CSS and design system consultancy</a>, if you paid me £1 for every time I heard something like this, I would have retired comfortably by now.</p>
<p>What I <em>did</em> find in these engagements was a culture of hand-off, rather than a culture of collaboration. My recommendation was almost never to embrace a certain framework to “fix problems”, but instead to build better production processes between designers and developers that are rooted in communication, respect and empathy. The tech choices simply are not important until that is sorted.</p>
<p>I’m going to write more about this stuff in the coming months because I think it’s really important, but in short, you can simplify <em>a lot</em> by not chucking designs over the fence and expecting them to be built verbatim. Work instead in cycles and prototypes to test ideas before the main production build.</p>
<p>The final deliverable of a web-based design is <strong>on the web</strong>, just like the final deliverable of a print design <strong>is print</strong>. Instead of handing off, designers and developers need to work in collaboration with each other in design implementation with a culture of <strong>flexibility</strong>.</p>
<p>Is something not working out as expected? Iterate in the browser as a team with prototypes to get a better output that works for everyone. Even if this means going back to design tools initially, make no absolute decisions about pictures of websites, but instead, make them about actual websites.</p>
<p>Looping back to progressive enhancement, all of the above will assist in that. Working in the actual deliverable’s medium — the web — in cycles/iterations/sprints, with progressive enhancement at the root will — I promise — result in smaller codebases, simpler UIs and happier users!</p>
<h2>Lastly, it’s not a quick fix</h2>
<p>Sorry! Progressive enhancement is not a command you can run in terminal or a package you can install. It’s a complete shift in mental model. It’s pretty damn painful if you’re coming from the  traditional <a href="https://resilientwebdesign.com/">collective hallucination of pixel perfection</a> too, I’m afraid.</p>
<p>It is worth pursuing though, I promise. Over the 15+ years of my career, <a href="https://piccalil.li/author/andy-bell">working with some of the largest organisations in the world</a>, <strong>nothing</strong> has come close to being as effective as progressive enhancement on <strong>all fronts</strong>.</p>
<p>Think of it like a design system. You know that’s gonna be long-winded process and expensive to set up to be a major part of your organisation’s workflow. The up front pain is worth it though because in the long term, design systems that are well maintained and documented are so often a huge cost saver.</p>
<p>The same goes for progressive enhancement. It’s <em>hard</em> to embrace, but let me tell ya, picking up a 12+ month old project that Just Works™ rather than spend ages updating packages — or in the worst case, declare the project “dead” — is a huge momentum boost. Hours of bug fixing and quirks are often saved too, as a nice bonus.</p>
<p>The speed and cheapness of ongoing feature additions and maintenance of a progressively enhanced project is unmatched. The other option is compounding technical debt and the risk that one day, that very specific, fragile mentality of pixel perfection is what ends up costing a fortune in the long run.</p>
<h2>Now you know, it’s time to make it happen</h2>
<p>And we’re here to help! I’ve been prattling on about progressive enhancement for years, but admittedly, I’m not providing tangible strategies that can be used in the real world. That’s hard for me to do without knowing what <strong>your real world constraints are</strong>, so please tell me.</p>
<p>Let us know and we’ll produce content that actually helps you! I don’t think progressive enhancement needs renaming either. I think instead, content tuned to people’s real world contexts is what makes it resonate more universally. The industry feels like it’s shifting in the right direction (if you ignore the obsession with AI), so a meaningful push towards progressive enhancement seems like a good idea to me, anyway.</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>How a minimum viable experience produces a resilient, inclusive end-product</title>
        <link>https://piccalil.li/blog/how-a-minimum-viable-experience-produces-a-resilient-inclusive-end-product/?ref=progressive-enhancement-category-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Mon, 27 Feb 2023 13:07:30 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/how-a-minimum-viable-experience-produces-a-resilient-inclusive-end-product/?ref=progressive-enhancement-category-rss-feed</guid>
        <description><![CDATA[<p>The whole idea of progressive enhancement is using the power that the web platform gives us for free — specifically, HTML, CSS and JavaScript — to provide a baseline experience for the people who visit our sites and/or apps, and <em>then</em> build on that where appropriate and necessary, depending on the capabilities of the technology that they are using.</p>
<p>These capabilities can and do vary hugely. A very large cohort of people that use the internet, do so on <a href="https://gs.statcounter.com/os-market-share/mobile/worldwide">Android devices (72% + market global share)</a>, often on <a href="https://en.wikipedia.org/wiki/List_of_countries_by_Internet_connection_speeds">slow connections</a>. This makes the probability of the browser environment being <strong>hostile</strong> to websites and apps that are distributed with a single point of failure, very high. This is often the case with client-side JavaScript-heavy single page applications, known as SPAs.</p>
<p>We can mediate this weakness with progressive enhancement, though, and a <strong>minimum viable experience</strong> approach helps us to use a <a href="https://adactio.com/journal/14327">principle of least power</a> to make sure that <strong>everyone</strong> gets a great experience.</p>
<h2>What is a minimum viable experience?</h2>
<p>I started using this term as a nod to a more commonly used term: <strong>minimum viable product</strong>, which is a the minimum version of a product that satisfies the purpose of itself, making it worth existing in the first place.</p>
<p>A good example of a minimum viable experience is an email client that is just a HTML form that when submitted, sends an email to the specified address. You could then enhance this minimum viable experience <strong>if</strong> JavaScript is available by adding client-side validation, auto-complete and all of the other handy tricks our modern email clients give us. The important thing is that <strong>when</strong> JavaScript fails, we can still provide the basic capability that the user needs.</p>
<p>The real beauty of progressive enhancement is that if you really take it seriously, a user won’t even notice if they don’t have the “optimal experience”. The fact they have an acceptable experience at all — a minimum viable experience — is good enough, as mentioned in <a href="https://www.youtube.com/watch?v=5uhIiI9Ld5M">this talk</a> I did last year.</p>
<p><img src="https://andy-bell.imgix.net/2023/02/diagram-1024x692.png" alt="" /></p>
<p>This sort of diagram is often used to illustrate a minimum viable product, but I think it perfectly illustrates what progressive enhancement is.</p>
<p>The top row shows four steps with a car as the optimal experience, right at the end. The problem is that in the preceding three steps, the experience is either completely broken or unusable. A car is useless with just one wheel or with no windows or roof (especially in rain-sodden England where I am based). It’s only when everything works perfectly when this experience is optimal, which in the case of the web, knowing that the likelihood that everything will work perfect is <em>extremely low</em>, seems like a suboptimal approach.</p>
<p>Step in the second row, which has the same four steps. The difference here is instead of a single wheel: step 1 is a skateboard. Instead of a chassis and wheels only, step 2 is a micro scooter. Step 3 is a push bike, instead of a car with no roof or windows, and finally, step 4 is a motor scooter instead of a car. Every step is acceptable and gets the user from A to B. It just so happens that with each <em>progression</em>, the experience gets better.</p>
<p>I deliberately chose the motor scooter as the optimal experience in this illustration because if you work with a progressive enhanced mindset — creating a minimum viable experience — you more often-than-not, create a much lighter end-product. This is because you don’t need to mess around with backwards-compatibility hacks or polyfills.</p>
<h2>Identifying a minimum viable experience</h2>
<p>I’ll quickly run-down the process of how I identify a minimum viable experience and then, enhance it. I’ll use the context of <a href="https://jotter.space/">this jotter app I made</a>, for quick note-taking on the web.</p>
<p><img src="https://andy-bell.imgix.net/2023/02/CleanShot-2023-02-27-at-17.32.42@2x-1024x717.jpg" alt="The jotter app, where there is a large text box. It features a placeholder: “Add your day notes here”." /></p>
<p>First up: what’s the minimum viable experience? It’s a humble <code>&lt;textarea&gt;</code> element! HTML allows us to create something useful immediately, and all this app needs to work is a <code>&lt;textarea&gt;</code> to write notes in.</p>
<p>Now, how can we enhance that minimum viable experience? Firstly, we can add some CSS to make the <code>&lt;textarea&gt;</code> fill the space up and remove some default styles, like this:</p>
<pre><code>textarea {
  display: block;
  width: 100%;
  min-height: 100vh;
  border: none;
  background: transparent;
  font-size: 1.2rem;
  padding: 1rem;
  resize: none;
}
</code></pre>
<p>Along with some light CSS that provides our layout and look and feel, it’s all nice and simple, so far and things are nice and resilient.</p>
<p>Right about now is the time to start adding some JavaScript. <a href="https://jotter.space/">As you can see on the app</a>, there’s a toggle for dark mode and light mode because although honouring the colour scheme user preference by default is useful: you should give users a choice because frankly, they might not like your dark/light mode!</p>
<p>Next on the list of JavaScript <strong>enhancements</strong> of this app is offline storage, using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage">local storage</a> to save your notes as you type. This means that if you accidentally refresh your browser: you won’t lose what you were typing. It also means that your notes will be there if you come back another time. This is especially useful for shopping lists, I find.</p>
<p>Lastly, you can enhance it even further by using a <a href="https://developers.google.com/web/fundamentals/codelabs/offline">service worker to make it work completely offline</a> by putting all the assets into cache storage then routing all requests through the service worker. Suddenly, you’ve got an installable, progressive web app, that works offline!</p>
<p>Not bad for something that starts as a humble <code>&lt;textarea&gt;</code>, right?</p>
<p><img src="https://andy-bell.imgix.net/2023/02/CleanShot-2023-02-27-at-17.33.05@2x-1024x717.jpg" alt="The same jotter app as before, but this time, it says “Progressive Enhancement Rules” in the text box" /></p>
<h2>Wrapping up</h2>
<p>I appreciate that the above is a very simplified example, but it illustrates how a minimum viable experience approach to building for the web <em>really does make things scale</em>, far beyond the limited, fragile approach of heavy-duty, client-side JavaScript-driven development. Progressive enhancement is how so much of the web works already — especially the flexible, declarative programming languages that power it: HTML and CSS. <a href="https://medium.com/business-startup-development-and-more/7-ancient-abandoned-websites-that-still-work-63395b92b428">It’s why so many websites that are now decades old, still work</a>!</p>
<p>JavaScript is not something to turn your nose up at either. It’s extremely good at <em>enhancing a user’s experience</em>. <strong>When</strong> it doesn’t load, it doesn’t matter, because if you produce a solid minimum viable experience: the user can likely do what they came to do anyway.</p>
<p>If you prioritise the experience of your users over your experience developing something, trust me: <em>everyone</em> will be much happier overall.</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Build a fully-responsive, progressively enhanced burger menu</title>
        <link>https://piccalil.li/blog/build-a-fully-responsive-progressively-enhanced-burger-menu/?ref=progressive-enhancement-category-rss-feed</link>
        <dc:creator><![CDATA[Andy Bell]]></dc:creator>
        <pubDate>Fri, 15 Jan 2021 08:18:51 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/build-a-fully-responsive-progressively-enhanced-burger-menu/?ref=progressive-enhancement-category-rss-feed</guid>
        <description><![CDATA[<p>This is a <em>long</em> tutorial, so make sure you have plenty of time to get though it. Let’s dive in.</p>
<h2>Getting started</h2>
<p>We’re keeping things on The Platform™ in this tutorial, so no need to worry about build processes. We’re going to have a single HTML page, a CSS file and a couple of JavaScript files.</p>
<p>First up, grab these starter files that will give you the right structure.</p>
<p><a href="https://piccalilli.s3.eu-west-2.amazonaws.com/530fbbc391989e513daa5e59d74c88b9.zip">Download starter files</a></p>
<p>Extract those into the folder you want to work out of. It should look like this:</p>
<pre><code>├── css
│   └── global.css
├── images
│   └── logo.svg
├── index.html
├── js
│   ├── burger-menu.js
│   └── get-focusable-elements.js
</code></pre>
<p>These files are all empty (apart from the logo), so let’s start filling them up.</p>
<h2>Adding our HTML</h2>
<p>We’ll start with HTML, so open up <code>index.html</code> and add the following to it:</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="ie=edge" /&gt;
    &lt;title&gt;Build a fully-responsive, progressively enhanced burger menu&lt;/title&gt;
    &lt;link rel="stylesheet" href="https://unpkg.com/modern-css-reset/dist/reset.min.css" /&gt;
    &lt;link rel="stylesheet" href="css/global.css" /&gt;
    &lt;link rel="preconnect" href="https://fonts.gstatic.com" /&gt;
    &lt;link
      href="https://fonts.googleapis.com/css2?family=Halant:wght@600&amp;family=Hind:wght@400;500;700&amp;display=swap"
      rel="stylesheet"
    /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;script src="js/burger-menu.js" type="module" defer&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>This is our HTML shell and features all the relevant CSS, fonts and JavaScript files, pre-linked and ready to go. We’re using <a href="https://piccalil.li/blog/a-modern-css-reset/">this modern CSS reset</a> to give us some sensible defaults that we’ll build on with our custom CSS.</p>
<p>Let’s add some more HTML, first. Open up <code>index.html</code> again, and after the opening <code>&lt;body&gt;</code> tag, add the following:</p>
<pre><code>&lt;header class="site-head" role="banner"&gt;
  &lt;a href="#main-content" class="skip-link"&gt;Skip to content&lt;/a&gt;
  &lt;div class="wrapper"&gt;
    &lt;div class="site-head__inner"&gt;
      &lt;a href="/" aria-label="ACME home" class="site-head__brand"&gt;
        &lt;img src="images/logo.svg" alt="ACME logo" /&gt;
      &lt;/a&gt;
      &lt;burger-menu max-width="600"&gt;
        &lt;nav class="navigation" aria-label="primary"&gt;
          &lt;ul role="list"&gt;
            &lt;li&gt;
              &lt;a href="#"&gt;Home&lt;/a&gt;
            &lt;/li&gt;
            &lt;li&gt;
              &lt;a href="#"&gt;About&lt;/a&gt;
            &lt;/li&gt;
            &lt;li&gt;
              &lt;a href="#"&gt;Our Work&lt;/a&gt;
            &lt;/li&gt;
            &lt;li&gt;
              &lt;a href="#"&gt;Contact Us&lt;/a&gt;
            &lt;/li&gt;
            &lt;li&gt;
              &lt;a href="#"&gt;Your account&lt;/a&gt;
            &lt;/li&gt;
          &lt;/ul&gt;
        &lt;/nav&gt;
      &lt;/burger-menu&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/header&gt;
</code></pre>
<p>This is our main site header and it’s got a few bits that we’ll break down. First up, we have a skip link. A skip link allows a user to skip past the header and navigation and jump straight to the <code>&lt;main&gt;</code> element—which in our case—contains a simple <code>&lt;article&gt;</code>. It’ll be visually invisible by default and show on focus, when we get around to writing some CSS.</p>
<p>Next up, we have the brand element, which contains our placeholder logo. We’re using the <code>aria-label</code> attribute to provide the text to mainly assist screen readers. Very importantly, the <code>alt</code> on the image describes the image as “ACME logo”, with “ACME” being the name of the company.</p>
<p>Lastly, we have our main navigation—a classic unordered list of links in a <code>&lt;nav&gt;</code> element. It’s wrapped in a <code>&lt;burger-menu&gt;</code> element which is a <a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements">Custom Element</a>. This is a great example of HTML being an incredibly smart programming language because even though we’ve not defined what this element is or does yet, but the browser doesn’t care—it just continues doing what it’s doing, without any fuss. This capability helps us build this project <em>progressively</em>, which we’ll get into more, shortly.</p>
<p>We have an <code>aria-label</code> on our <code>&lt;nav&gt;</code> element. It’s not required in our case, but if you have more than one <code>&lt;nav&gt;</code> on a page, you must label them to help assistive technology.</p>
<p>Let’s wrap up our HTML by adding the last bit. Still inside <code>index.html</code>, add the following <strong>after</strong> the closing <code>&lt;/header&gt;</code>:</p>
<pre><code>&lt;main id="main-content" tabindex="-1" class="wrapper"&gt;
  &lt;article class="post flow"&gt;
    &lt;h1&gt;A responsive, progressively enhanced burger menu&lt;/h1&gt;
    &lt;p&gt;
      Burger menus are a relic of responsive design that no matter what your opinion of
      them is, they continue to be a dominant design pattern. They’re very good at
      preserving often-limited horizontal space, but they also, more often than not, are
      built in a user-hostile, non-accessible fashion.
    &lt;/p&gt;
    &lt;p&gt;
      &lt;a
        href="https://piccalil.li/premium/build-a-fully-responsive-progressively-enhanced-burger-menu"
        &gt;In this premium tutorial&lt;/a
      &gt;, we’re going to build a burger menu from the ground up, using progressive
      enhancement, `ResizeObserver`, `Proxy` state and of course, super-solid HTML and CSS
      that pull from the CUBE CSS principles.
    &lt;/p&gt;
    &lt;p&gt;
      This whole page is what you’re building in the tutorial 👆.
      &lt;a
        href="https://piccalil.li/premium/build-a-fully-responsive-progressively-enhanced-burger-menu"
        &gt;Let’s dive in&lt;/a
      &gt;
    &lt;/p&gt;
  &lt;/article&gt;
&lt;/main&gt;
</code></pre>
<p>There’s not much to say about this as it’s a pretty straightforward <code>&lt;article&gt;</code>. The only bit to make you aware of is that the <code>&lt;main&gt;</code> element should only feature once on the page, being the <strong>main content</strong>. We’ve got an <code>id</code> of <code>main-content</code> on there, too, which is what our skip link links to.</p>
<p>We’ve got all of our HTML now and guess what: we’ve got a fully functional web page (if you ignore the <code>#</code> links). The key to progressive enhancement is building up with a <a href="https://adactio.com/journal/14327">principle of least power approach</a>. We know now that as long as this very small HTML page lands in the user’s browser, they can immediately and effectively use it.</p>
<h2>Minimum Viable Experience CSS</h2>
<p>We’re going to approach our CSS progressively too, so using principles of <a href="https://piccalil.li//cube.fyi">CUBE CSS</a>: we’re going to start right at the top with some global CSS.</p>
<div><h2>FYI</h2>
  If you haven’t read up on CUBE CSS yet, I would recommend reading [this high-level overview post](/blog/cube-css/) to give yourself some background knowledge.
</div>
<p>Open up <code>css/global.css</code> and add the following to it:</p>
<pre><code>:root {
  --color-light: #ffffff;
  --color-light-shade: #fafffd;
  --color-dark: #062726;
  --color-primary: #d81159;
  --color-primary-shade: #b90f4c;
}
</code></pre>
<p>These are our site colours, neatly organised as some root-level CSS Custom Properties. The <code>:root</code> pseudo-class is just a posh way of using the <code>&lt;html&gt;</code> element. Keep in mind, though, that a pseudo-class (not pseudo-elements) has a higher specificity than a HTML element (<code>&lt;html&gt;</code> in this case). They have the same specificity as a <code>class</code>.</p>
<p>Let’s add some core globals. Still in <code>global.css</code>, add the following:</p>
<pre><code>body {
  background: var(--color-light-shade);
  color: var(--color-dark);
  line-height: 1.5;
  font-family: 'Hind', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-weight: 400;
}

h1,
h2 {
  font-family: 'Halant', Georgia, 'Times New Roman', Times, serif;
  font-weight: 600;
  line-height: 1.1;
  max-width: 30ch;
}

h1 {
  font-size: 2rem;
}

h2 {
  font-size: 1.8rem;
}

a {
  color: currentColor;
}

:focus {
  outline: 1px dotted currentColor;
  outline-offset: 0.2rem;
}

p,
li,
dl {
  max-width: 70ch;
}

article {
  margin-top: 2.5rem;
  font-size: 1.25rem;
}

main:focus {
  outline: none;
}

@media (min-width: 40em) {
  h1 {
    font-size: 3rem;
  }

  h2 {
    font-size: 2.5rem;
  }
}
</code></pre>
<p>These are high-level, global HTML-element styles. We’re setting the basics, mainly, but again, with progressive enhancement in mind, we’re keeping things as simple as possible.</p>
<p>Some key points:</p>
<ol>
<li>We set a <code>max-width</code> on headings, paragraphs, lists elements and description lists using a <code>ch</code> unit. This really helps with readability and a <code>ch</code> unit is equal to the width of a <code>0</code> character in your chosen font and size. You can <a href="https://piccalil.li/quick-tip/limit-line-lengths-to-increase-readability/">read more about why we do this here</a>.</li>
<li>We set <code>:focus</code> styles globally by modifying how the <code>outline</code> looks. This means that any element that can receive focus, such as <code>&lt;a&gt;</code> and <code>&lt;button&gt;</code> will have a consistent focus style. The <code>outline-offset</code> pulls the outline away from the content a bit, which in my opinion, makes it more user-friendly.</li>
<li>We remove focus styles from the <code>&lt;main&gt;</code> element because when someone activates the skip link from before, it programatically focuses the <code>&lt;main&gt;</code> because it’s the <code>:target</code>. The focus ring is unnecessary though, because making the <code>&lt;main&gt;</code> focusable, programatically, is purely for making tabbing on the keyboard more predictable for users who want to skip navigation. If we didn’t move focus, they could end up in a situation where hitting the tab key sends them back up the the navigation!</li>
</ol>
<div><h2>FYI</h2>
  Did you know that setting a `tabindex` HTML attribute value of `-1` allows you to focus it programatically? What this means is that you can—just like the skip link—pass focus when it’s targetted, but you can also use JavaScript to pass focus, using the `focus()` function.
<p>What’s handy with this approach is that using <code>tabindex="-1"</code> prevents the user from being able to tab to the element with their keyboard too, so it’s really helpful with focus management of interactive elements, which we’ll get on to later in this tutorial.
</p>
<h3>CSS Utilities</h3>
<p>We’ve got some sensible, global CSS, so let’s now add some utilities.</p>
<p>In the <a href="https://cube.fyi/utility/">CUBE-CSS documentation</a>, I describe utilities like so:</p>
<blockquote>
<p>A utility, in the context of CUBE CSS, is a CSS class that does one job and does that one job well.</p>
</blockquote>
<p>With that in mind, we’re going to add 3 utilities: a skip link, a wrapper and finally a <a href="https://piccalil.li/quick-tip/flow-utility">flow utility</a>.</p>
<p>Let’s add the skip link first. Open up <code>global.css</code> and add the following to it:</p>
<pre><code>.skip-link {
  display: inline-block;
  padding: 0.7rem 1rem 0.5rem 1rem;
  background: var(--color-light);
  color: var(--color-primary-shade);
  text-decoration: none;
  font-weight: 700;
  text-transform: uppercase;
  position: absolute;
  top: 1rem;
  left: 1rem;
}
</code></pre>
<p>The skip link is styled to look like a button. It’s good to give it plenty of contrast against it’s context—which in our case, is the site header—and making it look like a button really helps with that. Other than that, there’s not much to discuss about this, so let’s add the clever stuff.</p>
<div><h2>FYI</h2>
  You might be wondering why we’re lumping all of the CSS into `global.css`. It’s to keep this tutorial as simple as possible. I would recommend breaking global CSS, utilities, blocks and composition styles (both coming up, shortly) into their own areas, in a proper project, though.
</div>
<p>Still inside <code>global.css</code>, add the following:</p>
<pre><code>.skip-link:hover {
  background: var(--color-dark);
  color: var(--color-light-shade);
}

.skip-link:not(:focus) {
  border: 0;
  clip: rect(0 0 0 0);
  height: auto;
  margin: 0;
  overflow: hidden;
  padding: 0;
  position: absolute;
  width: 1px;
  white-space: nowrap;
}
</code></pre>
<p>The first part is some good ol’ <code>:hover</code> styles. The important part, though, is that when the skip link is <strong>not focused</strong>, we <strong>visually hide it</strong>. The CSS in the <code>.skip-link:not(:focus)</code> block is the same as <a href="https://piccalil.li/quick-tip/visually-hidden">this visually hidden utility</a>, which allows screen readers and parsers to “see” it, while visually, it isn’t present. Because we use the <code>:not(:focus)</code> pseudo-selector: when the skip link is focused, it shows, visually.</p>
<p>Right, that’s our main utility sorted. This one very much blurs the line between a CUBE CSS block and a utility, but I’m pretty happy putting it where we have it for this one.</p>
<p>Next up, let’s add those remaining utilities. We’ll add the <code>wrapper</code> which is a simple container that helps keep our sections aligned with each other.</p>
<p>Open up <code>global.css</code> and add the following to it:</p>
<pre><code>.wrapper {
  max-width: 65rem;
  margin-left: auto;
  margin-right: auto;
  padding-left: 1.25rem;
  padding-right: 1.25rem;
}
</code></pre>
<p>This does exactly what it says on the tin, really. The only part to mention is that we specifically add left/right padding/margin so <a href="https://cube.fyi/composition">other compositional CSS</a> can comfortably manage vertical space, if needed.</p>
<p>Let’s add the flow utility, too. Inside <code>global.css</code>, add the following:</p>
<pre><code>.flow &gt; * + * {
  margin-top: var(--flow-space, 1em);
}
</code></pre>
<p>This utility is <a href="https://piccalil.li/quick-tip/flow-utility">explained in this quick tip</a>, so go ahead and give it a read, but in short, it adds space to sibling elements automatically and I use it <strong>all the time</strong>.</p>
<p>With all of the HTML, global CSS and CSS utilities added, your page should look like this.</p>
<p><img src="https://piccalil.b-cdn.net/images/tutorials/bm-ss-1.jpg" alt="A very basic page, with mostly HTML with a touch of CSS" /></p>
<h3>CSS Blocks</h3>
<p>Let’s dig into the details now—but we’re only going to do our minimum viable experience CSS.</p>
<p>This is the default experience, for <strong>when</strong> JavaScript doesn’t load for a user. We want to make sure that the experience is solid and that the user doesn’t even notice that there is missing functionality.</p>
<p>We build this CSS first, but it’s also worth continually testing this use-case <em>even when</em> all your main functionality—in our case, a burger menu—is fully implemented.</p>
<p>Let’s start by styling up the site head. Open up <code>global.css</code> and add the following to it:</p>
<pre><code>.site-head {
  padding: 0.6rem 0;
  background: var(--color-primary);
  border-top: 5px solid var(--color-primary);
  border-bottom: 5px solid var(--color-primary-shade);
  color: var(--color-light);
  line-height: 1.1;
}

.site-head :focus {
  outline-color: var(--color-light);
}

.site-head__inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 0 1rem;
}

.site-head__brand {
  display: block;
  width: 3rem;
}
</code></pre>
<p>To start with, we add the colour to site head and a nice bottom border. Having just a bottom border puts things out of kilter, visually, so we add an optical adjustment, in the form of the same border style—<em>but</em> the same colour as the background. This border is essentially invisible, but it levels things out. Brains are weird, right?</p>
<p>Next, some good ol’ flexible layout stuff. The <code>site-head__inner</code> element uses flexbox to <em>push</em> elements away from each other—importantly, only where there is space. We use <code>flex-wrap: wrap</code> to allow items to stack on top of each other where needed.</p>
<p>All mixed together, this means that because <code>justify-content</code> affects the <strong>horizontal axis</strong>—because we are using the default <code>flex-direction</code> value of <code>row</code>—it won’t affect items that are not on the same axis anymore. This means we get responsive layout with no media queries. Handy.</p>
<p>Let’s move on to a navigation block. Open up <code>global.css</code> again and add the following:</p>
<pre><code>.navigation ul {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem 0.8rem;
  padding: 0;
}

.navigation li {
  margin: 0.1rem;
}

.navigation a {
  font-weight: 600;
  text-transform: uppercase;
  text-decoration: none;
  color: currentColor;
}

.navigation a:hover {
  color: var(--color-dark);
}
</code></pre>
<p>The first thing to note here is that we don’t have any specific styles for the <code>.navigation</code> block itself. This is because with CUBE CSS, <a href="https://cube.fyi/block/#heading-what-should-a-block-do">a block is more of a namespace</a>. Diving into the list element, though, we’re using the same sort of flexible layout approach as we did in the <code>site-head</code> block.</p>
<p>We’re using <code>gap</code> to space elements and again, allowing items to fall onto a new line if there’s no space. This gives us a handy, extremely acceptable, <a href="https://piccalil.li/blog/a-minimum-viable-experience-makes-for-a-resilient-inclusive-website-or-app">minimum viable experience</a>, regardless of viewport size.</p>
<p>We do provide a cheeky little fallback for if <code>gap</code> isn’t supported (<a href="https://caniuse.com/flexbox-gap">at the time of writing, Safari, mainly</a>). By adding a tiny, <code>0.1rem</code> margin on <strong>all sides</strong> of a list item. You could use <code>@supports</code> to remove this for <code>gap</code> support, but remember, <strong>principal of least power</strong>. Also, <a href="https://ishadeed.com/article/flexbox-gap/">it’s tricky to detect</a>.</p>
<p>The rest of this block is pretty darn self-explanatory and we still have <em>a lot</em> to cover, so let’s write some JavaScript.</p>
<p>First, take a look at your lush, Minimum Viable Experience:</p>
<p><img src="https://piccalil.b-cdn.net/images/tutorials/bm-ss-2.jpg" alt="A fully styled page with a bright reddish pink main site head" /></p>
<h2>JavaScript</h2>
<p>Now to move on to the main course of this tutorial. Have a quick stretch, get a drink, then get comfy, because this is where we’re going to really <em>dig in</em> to the details.</p>
<h2>Burger menu web component</h2>
<p>We’ll start with the main component itself. Open up <code>js/burger-menu.js</code> and add the following.</p>
<pre><code>class BurgerMenu extends HTMLElement {
  constructor() {
    super();

    const self = this;

    this.state = new Proxy(
      {
        status: 'open',
        enabled: false
      },
      {
        set(state, key, value) {
          const oldValue = state[key];

          state[key] = value;
          if (oldValue !== value) {
            self.processStateChange();
          }
          return state;
        }
      }
    );
  }

  get maxWidth() {
    return parseInt(this.getAttribute('max-width') || 9999, 10);
  }

  connectedCallback() {
    this.initialMarkup = this.innerHTML;
    this.render();
  }

  render() {
    this.innerHTML = `
      &lt;div class="burger-menu" data-element="burger-root"&gt;
        &lt;button class="burger-menu__trigger" data-element="burger-menu-trigger" type="button" aria-label="Open menu"&gt;
          &lt;span class="burger-menu__bar" aria-hidden="true"&gt;&lt;/span&gt;
        &lt;/button&gt;
        &lt;div class="burger-menu__panel" data-element="burger-menu-panel"&gt;
          ${this.initialMarkup} 
        &lt;/div&gt;
      &lt;/div&gt;
    `;

    this.postRender();
  }
}

if ('customElements' in window) {
  customElements.define('burger-menu', BurgerMenu);
}

export default BurgerMenu;
</code></pre>
<p>We’ve got a heck of a chunk of JavaScript here, but fear-not, we will go through what is happening.</p>
<p>First of all, we’re creating a <a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements">custom element—a web component</a>. We instantiate a new class which extends <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement"><code>HTMLElement</code></a>, which is the basis of <strong>all HTML elements</strong>.</p>
<div><h2>FYI</h2>
  A custom element uses JavaScript classes. If you’ve not done much with these already, I definitely recommend that you have a quick break from this (don’t worry, we’ll wait) to [brush up on how they work with this handy resource](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes).
</div>
<p>Inside our <code>BurgerMenu</code> class: we’ve got the constructor, which initialises everything when it’s loaded. The first thing we do is call <code>super()</code>, which tells our extended <code>HTMLElement</code> to do the same.</p>
<p>The next part is one of my favourite features of modern JavaScript: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy">Proxies</a>. These are a handy type of object that let us do <em>loads</em> of cool stuff, but my favourite part is that we can <strong>observe and manipulate changes</strong>. <a href="https://piccalil.li/tutorial/build-a-light-and-global-state-system">I wrote up about them in this tutorial</a> and we’re using a similar setup to manage our component’s <code>state</code>.</p>
<p>In a nutshell, we create a <code>set</code> method in our <code>Proxy</code> which gets fired every time a value of our <code>state</code> is changed. For example, if elsewhere in our code, we write <code>state.enabled = false</code>, the <code>set</code> method of our <code>Proxy</code> is fired and it’s <em>in here</em> where we intercept and commit that change.</p>
<p>Alongside committing that change, we’re comparing the old value to the new value. If that value has changed, we fire of a yet-to-exist, <code>processStateChange()</code> method. We’ve got some light, reactive state going on here and it’s all baked into vanilla JavaScript. Cool, right?</p>
<p>Next up, we use a handy modern JavaScript feature, a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get">getter</a> to grab the <code>max-width</code> property from our <code>&lt;burger-menu&gt;</code> instance. This gives the component a <code>maxWidth</code> value, which we’ll use later to determine wether or not to enable our burger menu.</p>
<p>After this getter, we come across a <a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#using_the_lifecycle_callbacks">lifecycle callbacks</a>. These—if defined in your component—fire off at various points in a component’s lifecycle. In this instance, the <code>connectedCallback</code> fires when our <code>&lt;burger-menu&gt;</code> is appended to the document. Think of it as a “ready” state. Now we know our component is connected, we’re going to tell our component to <code>render()</code>.</p>
<p>Before we do that, though, we store the markup that’s inside the <code>&lt;burger-menu&gt;</code>, which in our case is the <code>&lt;nav&gt;</code> element and it’s content. We store it for two reasons:</p>
<ol>
<li>We’re going to render that same markup inside our component markup</li>
<li>If all fails in this component, we can re-render the markup as if there was no burger menu whatsoever</li>
</ol>
<p>Moving swiftly onto the <code>render()</code> method: we’re using a template literal to write out some HTML. You’ll notice inside that HTML, we call on our <code>initialMarkup</code>, which we just stored. The markup is pretty straightforward. It’s a trigger button and an associated panel—similar to a <a href="https://piccalil.li/tutorial/a-progressive-disclosure-component">disclosure element</a>.</p>
<p>Lastly, we apply this component using the <code>customElements.define('burger-menu', BurgerMenu);</code> line <em>only</em> if <code>customElements</code> is available—another bit of progressive enhancement. We also <code>export</code> the class as a <code>default</code> export for if someone was to <code>import</code> this component.</p>
<p>Finally, after all this is set, we move on to <code>this.postRender()</code> which we will add now. Still in <code>burger-menu.js</code>, <strong>under the <code>render()</code> method</strong>, add the following:</p>
<pre><code>postRender() {
  this.trigger = this.querySelector('[data-element="burger-menu-trigger"]');
  this.panel = this.querySelector('[data-element="burger-menu-panel"]');
  this.root = this.querySelector('[data-element="burger-root"]');
  this.focusableElements = getFocusableElements(this);

  if (this.trigger &amp;&amp; this.panel) {
    this.toggle();

    this.trigger.addEventListener('click', evt =&gt; {
      evt.preventDefault();

      this.toggle();
    });

    document.addEventListener('focusin', () =&gt; {
      if (!this.contains(document.activeElement)) {
        this.toggle('closed');
      }
    });

    return;
  }

  this.innerHTML = this.initialMarkup;
}
</code></pre>
<p>There’s quite a bit going on in this method, whose role is to handle the fresh new HTML that’s just been rendered.</p>
<p>The first thing we do is grab the elements we want. I personally like to use <code>[data-element]</code> attributes for selecting elements with JavaScript, but really, do whatever works for you and your team. I certainly don’t do it for any good reason other than it makes it more obvious what elements have JavaScript attached to them.</p>
<p>The next thing we do is test to see if the <code>trigger</code> and <code>panel</code> are <strong>both</strong> present. Without both of these, our burger menu is redundant. If they are both there, we fire off the yet-to-be-defined <code>toggle()</code> method and wire up a click event to our <code>trigger</code> element, which again, fires off the <code>toggle()</code> method.</p>
<p>The next part is an accessibility pro tip. The burger menu—when we finish applying all of the CSS—covers the entire viewport. This means that if the user is shifting focus with their tab key and focus escapes the burger menu itself: they will lose focus <em>visually</em>. This is a poor user experience, so this <code>focusin</code> event listener on the <code>document</code>, <em>outside</em> of this component tests to see if the currently focused element—<code>document.activeElement</code>—is inside our component. If it isn’t: we force the menu closed, immediately.</p>
<p>Lastly, as a last-ditch fallback, we re-render the original markup. This is to make sure that if all fails, the user still gets the minimum viable experience. Don’t you just <em>love</em> the smell of progressive enhancement?</p>
<p>Let’s define that <code>toggle()</code> method. Still inside the <code>burger-menu.js</code> file, add the following <strong>after</strong> the <code>postRender()</code> method:</p>
<pre><code>toggle(forcedStatus) {
  if (forcedStatus) {
    if (this.state.status === forcedStatus) {
      return;
    }

    this.state.status = forcedStatus;
  } else {
    this.state.status = this.state.status === 'closed' ? 'open' : 'closed';
  }
}
</code></pre>
<p>In <code>toggle()</code>, we can pass an optional <code>forcedStatus</code> parameter which—just like in the above focus management—let’s us force the component into a specific, finite state: <code>'open'</code> or <code>'closed'</code>. If that isn’t defined, we set the current <code>state.status</code> to be open or closed, depending on what the current status is, using a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator">ternary operator</a>.</p>
<p>Now that we’ve got a state toggle, let’s process that state. We’ll add the method that is called in our <code>Proxy</code>: <code>processStateChange()</code>. Still in <code>burger-menu.js</code>, add the following <strong>after</strong> the <code>toggle()</code> method:</p>
<pre><code>processStateChange() {
  this.root.setAttribute('status', this.state.status);
  this.root.setAttribute('enabled', this.state.enabled ? 'true' : 'false');

  this.manageFocus();

  switch (this.state.status) {
    case 'closed':
      this.trigger.setAttribute('aria-expanded', 'false');
      this.trigger.setAttribute('aria-label', 'Open menu');
      break;
    case 'open':
    case 'initial':
      this.trigger.setAttribute('aria-expanded', 'true');
      this.trigger.setAttribute('aria-label', 'Close menu');
      break;
  }
}
</code></pre>
<p>This method is fired every time state changes, so its only job is to grab the current state of our component and reflect it where necessary. The first part of that is setting our root element’s attributes. We’re going to use this as style hooks later. Then, we set the <code>aria-expanded</code> attribute and the <code>aria-label</code> attribute on our trigger. We’ll do the actual visual toggling of the panel with CSS.</p>
<div><h2>FYI</h2>
  We’re using a disclosure pattern to do the visual toggling of this component. I wrote a tutorial on that a while ago that explains how the `aria-expanded` attribute works. [Check it out](https://piccalil.li/tutorial/a-progressive-disclosure-component)!
</div>
<p>We’re getting close now, pals, hang in there. This is a <em>long</em> tutorial, but heck, we are creating something pretty darn resilient. Let’s wrap up the JavaScript with some focus management, then get back to the comfort and warmth of CSS.</p>
<p>We referenced a <code>manageFocus()</code> method earlier that we need to write, so still in <code>burger-menu.js</code>, <strong>after</strong> the <code>processStateChange</code> method, add the following:</p>
<pre><code>manageFocus() {
  if (!this.state.enabled) {
    this.focusableElements.forEach(element =&gt; element.removeAttribute('tabindex'));
    return;
  }

  switch (this.state.status) {
    case 'open':
      this.focusableElements.forEach(element =&gt; element.removeAttribute('tabindex'));
      break;
    case 'closed':
      [...this.focusableElements]
        .filter(
          element =&gt; element.getAttribute('data-element') !== 'burger-menu-trigger'
        )
        .forEach(element =&gt; element.setAttribute('tabindex', '-1'));
      break;
  }
}
</code></pre>
<p>Here, we look grab our focusable elements (we’re doing that bit next) and then depending on wether or not we’re in an open or closed, state, we add <code>tabindex="-1"</code> or remove it. We add it when we in a closed state because if you remember rightly, this prevents keyboard focus. For the same reason we automatically closed the menu when focus escaped in the open state, earlier, we are now preventing focus from leaking in if it is closed.</p>
<div><h2>FYI</h2>
  You might be thinking, “what the heck is Andy doing with `[...`??”. This is called the [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) and what we are doing here is converting our `NodeList` into an `Array` so we can use the `filter()` method to filter out the `trigger` element.
</div>
<p>We now need to add a mechanism that enables or disables the burger menu UI, based on the <code>maxWidth</code> property. To do that, we’re going to use a <a href="https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver"><code>ResizeObserver</code></a> which does exactly what it says on the tin: observes resizing.</p>
<p>Go back to the <code>connectedCallback()</code> method and <strong>inside it</strong>, add the following:</p>
<pre><code>const observer = new ResizeObserver(observedItems =&gt; {
  const {contentRect} = observedItems[0];
  this.state.enabled = contentRect.width &lt;= this.maxWidth;
});

// We want to watch the parent like a hawk
observer.observe(this.parentNode);
</code></pre>
<p>The <code>ResizeObserver</code> gives us a callback, every time the observed element changes size—in our case, the <code>&lt;burger-menu&gt;</code> parent, which happens to be <code>.site-head__inner</code>—we can monitor it and if needed, react to it.</p>
<p>We <a href="https://css-tricks.com/learning-gutenberg-4-modern-javascript-syntax/#destructuring-assignment">destructure</a> the <code>contentRect</code> out of the first item in <code>observedItems</code> (our <code>.site-head__inner</code> element) which gives us its dimensions. We then set our <code>state.enabled</code> flag based on wether or not is is less than, or equal to, our <code>maxWidth</code> property.</p>
<p>You might think that doing this in JavaScript is daft, but here me out: this is a low-level Container Query! It means this burger menu could be put <em>anywhere</em> in a UI. The <code>ResizeObserver</code> is <em>super</em> performant too, so there’s not much to worry about on that front.</p>
<p>Right, let’s add the final piece of the JavaScript puzzle: the helper method that finds all focusable elements for us. You can <a href="https://piccalil.li/quick-tip/load-all-focusable-elements-with-javascript">read up on how it works, here</a>.</p>
<p>First up, still inside the <code>buger-menu.js</code> file, add the following <strong>right at the top of the file</strong>:</p>
<pre><code>import getFocusableElements from './get-focusable-elements.js';
</code></pre>
<p>All we’re doing here is importing our helper, so let’s go ahead and write that method. Open up <code>get-focusable-elements.js</code> and add the following to it:</p>
<pre><code>/**
 * Returns back a NodeList of focusable elements
 * that exist within the passed parnt HTMLElement
 *
 * @param {HTMLElement} parent HTML element
 * @returns {NodeList} The focusable elements that we can find
 */
export default parent =&gt; {
  if (!parent) {
    console.warn('You need to pass a parent HTMLElement');
    return [];
  }

  return parent.querySelectorAll(
    'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled]), details:not([disabled]), summary:not(:disabled)'
  );
};
</code></pre>
<p>This is just like a CUBE CSS utility, really. It does one job really well for us!</p>
<h2>Burger menu CSS</h2>
<p>We’ve got the burger menu interactivity written now and you might be happy to know that we <em>are done</em> with JavaScript. If you refresh your browser, it will be a <em>mess</em>, so let’s make it look good.</p>
<p>Open up <code>css/global.css</code> and add the following:</p>
<pre><code>.burger-menu__trigger {
  display: none;
}
</code></pre>
<p>Why the heck are we hiding the trigger? Well, think back to our <code>state.enabled</code> flag. If the component is disabled—which is our default state—we don’t want to present a trigger. Hiding it with <code>display: none</code> will hide it from screen readers, too.</p>
<p>Let’s build the actual hamburger icon. We’ll do it all with CSS, so still in <code>global.css</code>, add the following:</p>
<pre><code>.burger-menu__bar,
.burger-menu__bar::before,
.burger-menu__bar::after {
  display: block;
  width: 24px;
  height: 3px;
  background: var(--color-light);
  border: 1px solid var(--color-light);
  position: absolute;
  border-radius: 3px;
  left: 50%;
  margin-left: -12px;
  transition: transform 350ms ease-in-out;
}

.burger-menu__bar {
  top: 50%;
  transform: translateY(-50%);
}

.burger-menu__bar::before,
.burger-menu__bar::after {
  content: '';
}

.burger-menu__bar::before {
  top: -8px;
}

.burger-menu__bar::after {
  bottom: -8px;
}
</code></pre>
<p>The first thing to note here is that we’re using good ol’ pixel sizes because we really want some control of how this thing is sized. The first thing we do is target the <code>.burger-menu__bar</code> (which lives inside the trigger) and both its <code>::before</code> and <code>::after</code> pseudo-elements and make them <em>all</em> look the same as each other: a bar.</p>
<p>After this, we break off and target specific parts—so positioning the <code>.burger-menu__bar</code> dead-center with absolute positioning, which allows us to comfortably animate it, knowing it won’t affect layout. We then add <code>content: ''</code> to both the pseudo-elements so they render and push one up and one down. This gives us our hamburger!</p>
<p>We’ll leave this hamburger for now and deal with our <code>enabled</code> state in CSS.</p>
<h3>Handling the <code>enabled</code> state</h3>
<p>Our <code>BurgerMenu</code> enables and disables itself based on its parent’s width and its own <code>maxWidth</code> property. We need to handle this state with CSS.</p>
<p>We’re going to use the <a href="https://cube.fyi/exception/">CUBE CSS Exception principle</a> to do this, which means hooking into data attribute values in our CSS. The <code>BurgerMenu</code> sets an <code>[enabled="true|false"]</code> attribute on our <code>.burger-menu</code> component, so let’s deal with that.</p>
<p>Still in <code>global.css</code>, add the following:</p>
<pre><code>.burger-menu[enabled='true'] .burger-menu__trigger {
  display: block;
  width: 2rem;
  height: 2rem; /* Nice big tap target */
  position: relative;
  z-index: 1;
  background: transparent;
  border: none;
  cursor: pointer;
}

.burger-menu[enabled='true'] .burger-menu__panel {
  position: absolute;
  top: 0;
  left: 0;
  padding: 5rem 1.5rem 2rem 1.5rem;
  width: 100%;
  height: 100%;
  visibility: hidden;
  opacity: 0;
  background: var(--color-primary-shade);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
</code></pre>
<p>Because the burger menu is enabled, we can style up the trigger and the panel. The trigger is a transparent button, because it houses the burger bars we just created. We do make the button considerably bigger than them, though, so there’s a decent sized tap target.</p>
<p>For the panel, we make it fill the screen. We set the vertical overflow to be <code>auto</code> so long menus can be scrolled. Lastly, we make it hidden by using <code>opacity</code> and <code>visibility</code>.</p>
<div><h2>FYI</h2>
  Pro tip: if you set `visibility: hidden`, it will hide the element from a screen reader, so just be aware of that!
</div>
<p>Let’s add some more CSS. Inside <code>global.css</code>, add the following:</p>
<pre><code>.burger-menu[enabled='true'] .navigation ul {
  display: block;
}

.burger-menu[enabled='true'] .navigation ul &gt; * + * {
  margin-top: 2rem;
}

.burger-menu[enabled='true'] .navigation li {
  font-size: 1.5rem;
}
</code></pre>
<p>What we are doing here is converting our navigation into a stacked menu when the burger menu is enabled. This is where the enabled flag is super handy because we don’t need to rely on viewport-wide media queries and instead, we have full control over our specific context, instead. Ah, just leave me to dream about Container Queries for a second, will you?</p>
<p>Right, back from my dreaming, let’s wrap up with our interactive states! Add the following to <code>global.css</code>:</p>
<pre><code>.burger-menu[enabled='true'][status='open'] .burger-menu__panel {
  visibility: visible;
  opacity: 1;
  transition: opacity 400ms ease;
}

.burger-menu[enabled='true'][status='closed'] .burger-menu__panel &gt; * {
  opacity: 0;
  transform: translateY(5rem);
}

.burger-menu[enabled='true'][status='open'] .burger-menu__panel &gt; * {
  transform: translateY(0);
  opacity: 1;
  transition: transform 500ms cubic-bezier(0.17, 0.67, 0, 0.87) 700ms, opacity 500ms ease
      800ms;
}
</code></pre>
<p>The panel’s visibility can only be changed <strong>if</strong> the burger menu is enabled <strong>and</strong> the status is open. This is a great example of finite state giving our UI some real resilience and importantly, reducing the risk of presenting a broken or confusing state to our users.</p>
<p>When the panel is “open”, we transition the <code>opacity</code> to <code>1</code> and set <code>visibility</code> to <code>visible</code> to show it. I like to only add transitions in the changed state like this. It makes the UI much snappier when elements revert <em>immediately</em>.</p>
<p>The last section is pretty cool, too. In the “closed” state, we visually hide the navigation items with <code>opacity</code> and push them down with <code>transform</code>. When the panel is in the “open” state, we transition them back to being visible with full opacity. It gives us a lovely transition effect.</p>
<p>Right, let’s add the <em>last bit of code</em>. In <code>global.css</code>, add the following:</p>
<pre><code>.burger-menu[enabled='true'][status='open'] .burger-menu__bar::before {
  top: 0;
  transform: rotate(45deg);
}

.burger-menu[enabled='true'][status='open'] .burger-menu__bar::after {
  top: 0;
  transform: rotate(-45deg);
}

.burger-menu[enabled='true'][status='open'] .burger-menu__bar {
  background: transparent;
  border-color: transparent;
  transform: rotate(180deg);
}
</code></pre>
<p>This is our burger bars converting themselves into a close icon when the menu is open. We achieve this by first, setting the background and border of the main (central) bar to be transparent, then we rotate the pseudo-elements in opposite directions to create a cross. Lastly, we spin it all around by transitioning the whole thing 180 degrees.</p>
<p>Again, we’re using our state to determine this, which admittedly, creates some gnarly selectors, but it also provides resilience and solidity.</p>
<p>With that, <strong>we are done</strong>. If you refresh your browser, resize it and toggle the menu, it should all look like this.</p>
<p><video></video></p>
<h2>Wrapping up</h2>
<p>That was a hell of a long tutorial. Fair play to you for sticking it out! If your version is broken, you can go ahead and <a href="https://piccalilli.s3.eu-west-2.amazonaws.com/4d9533344b6ed90d0e81b5ce0421a31f.zip">download a complete copy, here</a>. You can also <a href="https://piccalilli-burger-menu-demo.netlify.app/">see a live version, here</a>.</p>
<p><a href="https://piccalilli.s3.eu-west-2.amazonaws.com/4d9533344b6ed90d0e81b5ce0421a31f.zip">Download final version</a></p>
<p>I hope you’ve learned a lot in this tutorial, but the main takeaway I’d love you to go away with is that even seemingly simple interactive elements—like burger menus—get really complex when you make them fully inclusive. Luckily, progressive enhancement makes that inclusivity a little bit easier.</p>
<p>Until next time, take it easy 👋</p></div>
        
        ]]></description>
        
      </item>
    
    </channel>
  </rss>
