Progressive enhancement
Tags: craftcode, javascript, progressiveenhancement, responsive
Published: 2023-11-20 09:31:36

8-9 minutes, 2161 words, 5th grade

Now that weʼve seen how semantic HTML alone produces a [fully-responsive web page](https://dev.to/craft-code/reponsive-by-default-mjb), where do we go next?

Our [plain HTML page](https://craft-code.dev/essays/code/responsive-by-default/example) is ugly as sin, so weʼre gonna need some CSS to spiff it up. But today letʼs keep the CSS simple. Weʼll return to good CSS practices soon in a follow-up article.

**Progressive enhancement** starts with something that is already functional and usable. Then it adds just enough JavaScript to enhance that functionality.

In this essay weʼll take a simple `<details>` element, which will work in any browser, and make it into an accordion. The `<details>` element with its `<summary>` is already an accordion. But it doesnʼt look like one.

## Start with the HTML

To begin, letʼs start with some essential HTML:

```html
<details class="xx-accordion">
  <summary class="xx-accordion-summary">Always visible</summary>
  <div class="xx-accordion-content">
    <p>Hidden content</p>
    <p>More hidden content</p>
  </div>
</details>
```

You can see an [example page here](https://craft-code.dev/essays/code/progressive-enhancement/html-only.html).

No doubt you have spotted the `class` attributes. We prefer to **apply CSS by class**. We can fall back to _class-and-tag-name_ if we need more specificity, as youʼll see below.

So whatʼs up with the “xx-” prefix?

Simple. To avoid **name clashes** with imported CSS, we namespace our classes. For example, the [Craft Code site](https://craft-code.dev/) might use “cc-” as in `cc-accordion`.

## Adding the initial CSS

Letʼs add a bit of CSS to give it that “accordion” look and feel. Here we go:

```css
.xx-accordion {
  background-color: #fafafa;
  border-radius: 5px;
  border: 1px solid #747481;
  color: #2c2c30;
}

.xx-accordion-summary {
  background-color: #0d4872;
  color: #fafafa;
  font-size: 1.25rem;
  margin: 0;
  padding: 1rem;
}

.xx-accordion-content {
  border-top: 1px solid #747481;
  padding: 0 1rem;
}
```

Again, we provide [an example page](https://craft-code.dev/essays/code/progressive-enhancement/basic-css.html).

This is already a workable accordion component. But most accordions have **more than one expandable element**. So letʼs add a few more elements:

```html
<div class="xx-accordion-group">
  <details class="xx-accordion">
    <summary class="xx-accordion-summary">
      Katherine Johnson
    </summary>
    <div class="xx-accordion-content">
      <p>
        <a
          href="https://en.wikipedia.org/wiki/Katherine_Johnson"
          rel="external"
        >Creola Katherine Johnson</a> was an American
        mathematician whose calculations of orbital mechanics
        as a NASA employee were critical to the success of
        the first and subsequent U.S. crewed spaceflights.
      </p>
    </div>
  </details>
  <details class="xx-accordion">
    <summary class="xx-accordion-summary">
      Dorothy Vaughan
    </summary>
    <div class="xx-accordion-content">
      <p>
        <a
          href="https://en.wikipedia.org/wiki/Dorothy_Vaughan"
          rel="external"
        >Dorothy Jean Johnson Vaughan</a> was an American
        mathematician  and human computer who worked for the
        National Advisory Committee for Aeronautics (NACA), and
        NASA, at Langley Research Center in Hampton, Virginia.
      </p>
    </div>
  </details>
  <details class="xx-accordion">
    <summary class="xx-accordion-summary">
      Mary Jackson
    </summary>
    <div class="xx-accordion-content">
      <p>
        <a
          href="https://en.wikipedia.org/wiki/Mary_Jackson_(engineer)"
          rel="external"
        >Mary Jackson</a> was an American mathematician and
        aerospace engineer at the National Advisory Committee
        for Aeronautics (NACA), which in 1958 was succeeded by
        the National Aeronautics and Space Administration (NASA).
      </p>
    </div>
  </details>
</div>
```

We have also adjusted the CSS a bit to make it work with multiple elements:

```css
.xx-accordion-group {
  border-radius: 5px;
  border: 1px solid #0d4872;
  padding: 0;
}

.xx-accordion {
  background-color: #fafafa;
  color: #2c2c30;
}

.xx-accordion-summary {
  background-color: #0d4872;
  border-bottom: 1px solid #fafafa;
  color: #fafafa;
  font-size: 1.25rem;
  margin: 0;
  padding: 1rem;
}

.xx-accordion:last-child summary {
  border-bottom: none;
}

.xx-accordion-content {
  padding: 0 1rem;
}
```

- We added the `.xx-accordion-group` properties. And moved the outer border from the individual elements to the group. We also changed the border color to match the `<summary>` background color.
- We removed the top border from the content block. Then we replaced it with a bottom border on the `<summary>` element. We also changed the color to the background color of the `<details>` elements. This makes it appear as a horizontal rule between the accordion elements.
- We donʼt want that bottom border on the last element in the accordion group. So we used `.xx-accordion:last-child summary` to remove that one bottom border.

Here is the [full accordion example](https://craft-code.dev/essays/code/progressive-enhancement/accordion-group.html).

## Enhancing our accordion

Rats! The accordion elements open and close instantly when you click on the summary. It would be nice if we animated the open and close.

We bet that your first thought will be that we can do this with **CSS animations**. We thought so, too. But it turns out that it is fiendishly difficult to make that work. By that we mean that we couldnʼt. Sigh.

CSS should always be your **first choice for effects**, but when CSS fails you, JavaScript steps up. We can do it the way we used to.

## CSS transitions for the loss

Naturally, the first thing we tried was a **CSS transition** on the height of the content block. But this only works if you know the **height of the block** in advance. It wonʼt work (for us, anyway) with height set to `auto` or `fit-content`.

OK, fine! Instead we set the open height to a CSS property with a fallback to auto: `var(--xx-accordion-height, auto)`. That oughta do it.

Now we need to set that property for each accordion element separately. Enter **JavaScript**!

## JavaScript to the rescue

The first thing we want to do is write a **function** that grabs any and all accordion elements on the page. Then weʼll add an **event listener** to each of them. And we want our function to run once as soon as the **DOM** has finished loading.

Here, then, is our first pass at the JavaScript:

```js
function enhanceAccordions () {
  const nodes = document
    .querySelectorAll(".xx-accordion-content")

  for (let node of nodes) {
    node.style =
      `--xx-accordion-height: ${node.clientHeight}px`
  }
}

globalThis.addEventListener(
  "DOMContentLoaded",
  enhanceAccordions
)
```

And our modified CSS:

```css
.xx-accordion-content {
  height: 0;
  overflow-y: hidden;
  padding: 0 1rem;
  transition: height 0.5s ease-in-out;
}

.xx-accordion[open] .xx-accordion-content {
  height: var(--xx-accordion-height, auto);
  transition: height 0.5s ease-in-out;
}
```

And it does work. Beautifully … the _first_ time we open an accordion element. And then it stops working. And it doesnʼt animate the close at all. Why not?

**Because toggling the `<details>` element sets “open” to `false` and thatʼs that.**

Slam.

OK, fine. Time to show it whoʼs boss.

## Kicking butt and taking names

First, letʼs **delete the CSS** we added. Forget using a transition! Weʼll do this the old-fashioned way.

First issue: when the `<details>` element toggles shut, it closes the accordion abruptly. We need to prevent that. To do so, we will seize control.

Instead of handling the `toggle` event, letʼs capture the `click` on the `<summary>` element. Then we will **prevent the click from bubbling up**.

We still need to know the height of the open accordion, but we donʼt need it in our CSS. So letʼs use a **property** instead. Weʼll call it `xxOpenHeight`. (Do remember to replace `xx` with your own namespace.)

```js
function toggleAccordion (event) {
  console.log(event.target.xxOpenHeight)
}

function enhanceAccordions () {
  const nodes = document.querySelectorAll(".xx-accordion")

  for (let node of nodes) {
    const summary = node.querySelector("summary")
    const content = node.querySelector(
      ".xx-accordion-content",
    )

    if (content) {
      summary.xxOpenHeight = content.clientHeight
    }

    summary?.addEventListener("click", toggleAccordion)
  }
}

globalThis.addEventListener("DOMContentLoaded", enhanceAccordions)
```

Check out the [enhanced example](https://craft-code.dev/essays/code/progressive-enhancement/accordion-group.html) and open the browser console. Note how clicking on the a summary prints the height of that accordionʼs content block to the console. Weʼre off to a good start.

Now letʼs complete the `toggleAccordion` function to **animate the accordion elements**. Weʼll explain this code line by line below.

```js
function toggleAccordion (event) {
  event.preventDefault()

  const summary = event.target
  const accordion = summary.closest("details")
  const content = accordion.querySelector(".xx-accordion-content")
  const openHeight = summary.xxOpenHeight

  if (accordion.open) {
    content.style.maxHeight = `${openHeight}px`

    function shutAccordion() {
      const maxHeight = parseInt(content.style.maxHeight, 10)

      if (maxHeight > 0) {
        content.style.maxHeight = maxHeight < 25
          ? "0"
          : `${maxHeight - 25}px`

        setTimeout(shutAccordion, 10)

        return
      }

      accordion.open = false
    }

    shutAccordion()

    return
  }

  content.style.maxHeight = "0px"

  function openAccordion() {
    const maxHeight = parseInt(content.style.maxHeight, 10)

    accordion.open = true

    if (maxHeight < openHeight) {
      content.style.maxHeight = `${maxHeight + 15}px`

      setTimeout(openAccordion, 10)
    }
  }

  openAccordion()
}
```

1. We need to prevent the element from **toggling**. We can do this with `event.preventDefault()` on line #2 above. Now we are responsible for opening and closing the accordion ourselves.
2. Next, we use the `event.target` to get the `<details> `(accordion) element, our content block, and the `xxOpenHeight`. Lines #4-7.
3. Now we have two possibilities. Either the accordion is **open** and we want to close it, or vice versa. So we check if the accordion is open on line # 9.
4. We need the `maxHeight` property of the content blockʼs `style` attribute. We will use it to **control the height** of the block. So if the accordion is open, then we must make sure that `maxHeight` it is set to `xxOpenHeight`. See line #10.
5. Now we will use `setTimeout` recursively to reduce the `maxHeight` bit by bit. Weʼll call it every few milliseconds until we have shut the accordion. We create our `shutAccordion` function on lines #12-26.
6. Hmm … `maxHeight` is a string. Urk. So we will **convert it to a number** with `parseInt` to decrement it. Then weʼll recreate the string. We get the integer on line #13. If our `maxHeight` is still greater than zero, we subtract 25 from it on lines #16-18. Then we call our `shutAccordion` function recursively. We set the timeout to 10 milliseconds. Thatʼs a nice frame rate.
7. When the `maxHeight` reaches zero, we set `accordion.open` to `false` (line #25) and weʼre **good to go**.
8. Now that weʼve defined our `shutAccordion` recursive function, we need to call it to **start the recursion** (animation). We do so on line #28.
9. But what if the accordion is closed? Then we set the `maxHeight` to zero on line #33 to be sure. This also covers the initial case.
10. We define our `openAccordion` recursive function on lines #35-45.
11. Again, we begin by parsing the height from `maxHeight` on line #36. And we immediately **set the accordion to “open”** on line #38. This has no visual effect yet because the height of the content block is currently zero.
12. Now, on lines #40-44, if the `maxHeight` is still less than the measured `openHeight`, we add 15 pixels to the `maxHeight`. Then we call the function again using `setTimeout` to delay for 10 milliseconds. When the content block is fully open, this conditional will fail and **the recursion will stop**.
13. And, as with the `shutAccordion` function above, we have to call the function to start the animation. We do this on line #47.

Not that difficult!

## Putting it all together

Try the final example above. You might note an issue with the content of the content block **becoming visible instantly**. This happens even though the height is zero.

That is because we havenʼt hidden the overflow. We can do that by adding `overflow-y: hidden` to the `.sb-accordion-content` CSS.

Once we do that, then our accordion should animate flawlessly. Now disable JavaScript in the DevTools of your browser. Check it out. It still works. It just doesnʼt animate. But we can live with that, right?

And if you **disable both CSS and JavaScript**, then _it still works fine!_ It even works on the [Lynx text-only browser](https://lynx.invisible-island.net/), which doesn't recognize the `<details>` element. But that just means that the accordion is always open.

Again, we can live with that.

Thatʼs progressive enhancement for you. Our code is simple and responsive and **works for everyone**. And if you have CSS and JS enabled, then you get a nicer experience.