---
title: Code Folding with 6 Lines of Vanilla JavaScript
subtitle: Or how to fold any element on a web page using the `<details>` tag
date: '2023-09-19'
slug: code-folding
---

Do you know what the top requested feature of **blogdown** has been in the past
six years? [Code folding](https://github.com/rstudio/blogdown/issues/214). I
have rarely seen 34 upvotes on a GitHub issue in projects that I maintain.

The original request was made in 2017 (thanks, Jasper Slingsby). Two years
later, when I was thinking about collapsing code blocks for **xaringan** slides,
[I experimented with the `<details>`
tag](https://github.com/yihui/xaringan/issues/219) (thanks, Emi Tanaka, [a cool
hacker](/en/2018/07/emi-tanaka/) as usual). I had a intuition that the
implementation of code folding could be extremely simple.

The natural question to ask is, why not just reuse this feature from the
**rmarkdown** package? Well, in terms of JS and CSS dependencies, **rmarkdown**
is too heavy and complicated in my eyes---there is a [JS
file](https://github.com/rstudio/rmarkdown/blob/main/inst/rmd/h/navigation-1.1/codefolding.js),
which is tied to Bootstrap and jQuery (two more things that I do not wish to
rely on), and the CSS code is scattered through a complicated [Pandoc
template](https://github.com/rstudio/rmarkdown/blob/main/inst/rmd/h/default.html).

On May 31st this year, [Xiangyun said](https://d.cosx.org/d/424459/13) that the
only missing feature of **blogdown** / Hugo sites he would want was code
folding. Unfortunately I went on [vacation](/en/2023/06/on-vacation/) soon, so I
did not look into it further. To close this browser tab (among dozens of
others), I spent a little more time on this task today.

## Six lines of JS to fold all code blocks

The idea is quite simple: create a `<details>` element containing a `<summary>`,
and move a code block (`<pre>`) into the `<details>` element.

``` js
document.querySelectorAll('pre').forEach(pre => {
  const d = document.createElement('details');
  d.innerHTML = '<summary>Details</summary>';
  pre.before(d);  // insert <details> before <pre>
  d.append(pre);  // move <pre> into <details>
});
```

That's it. You can open any web page that contains `<pre>` blocks, paste the
code into the JavaScript console of your browser (in Developer Tools), and see
code blocks being folded into `Details` elements, on which you can click to
unfold.[^1]

[^1]: You may not like details to "snap" on toggle, but prefer a smooth
    transition. That can be achieved by [more JS
    code](https://codepen.io/Yihui-Xie/pen/WNLdvVb) (which I forked and tweaked
    from [Louis Hoebregts's
    work](https://css-tricks.com/how-to-animate-the-details-element-using-waapi/),
    but honestly, I do not understand it).

## Fold source but not output blocks

If you want to fold only source code blocks but not output, it is not hard,
either. For HTML documents generated by **knitr** / R Markdown / Quarto, source
code blocks often have classes, and output blocks do not. We can tweak our
selector above based on this fact.

``` js
document.querySelectorAll('pre[class], pre > code[class]').forEach(el => {
  const d = document.createElement('details');
  d.innerHTML = '<summary>Details</summary>';
  const pre = el.tagName === 'CODE' ? el.parentNode : el;
  pre.before(d);  // insert <details> before <pre>
  d.append(pre);  // move <pre> into <details>
});
```

With `pre[class], pre > code[class]`, we are selecting two types of elements:
`<pre>` with classes, and `<pre>`'s direct child `<code>` with classes. This is
because different Markdown renderers can put the class on either `<pre>` or
`<code>`.

If you run the above code on a page, `<pre class="r">` and
`<pre><code class="r">` will be folded (the class name can be any other names),
but `<pre><code>` will not.

## Fold or unfold all blocks

You may also want a button to fold or unfold all blocks. Again, that is simple
to implement. For example, you can first provide a button on your HTML page:

``` html
<button id="toggle-all">Toggle Code</button>
```

Then add a `click` event to it via JavaScript:

``` js
document.getElementById('toggle-all').onclick = (e) => {
  [...document.getElementsByTagName('details')].forEach(el => {
    el.toggleAttribute('open');
  });
};
```

You do not have to use the `click` event of a button, but can use any event of
any element. The key here is to toggle the `open` attribute of `<details>`. If
it has the `open` attribute, it is unfolded, otherwise it is folded.

### Aside: a button from a decade ago

In fact, I have already implemented such a toggle button ten years ago, and the
JS code is [in the **knitr**
package](https://github.com/yihui/knitr/blob/master/inst/misc/toggleR.js) (I
just rewrote it with modern JS today). At that time, I was not aware of the
`<details>` tag in HTML, and used the CSS attribute `display` (`block` or
`none`) to control the visibility of code blocks.

## Fold anything: a general solution

Once you have learned [CSS
selectors](https://www.w3schools.com/cssref/css_selectors.php) (which are very
flexible), you can apply the above idea to fold anything on a web page. All you
need to do is provide an appropriate selector to `document.querySelectorAll()`.
For example, if you want to fold tables, just use `table` as the selector; or if
you want to fold the comments section `<section class="comments">`, use
`section.comments`.

I have written the script
[`fold-details.js`](https://github.com/yihui/misc.js/blob/main/js/fold-details.js)
and you can just load it on your page:

``` html
<script src="https://cdn.jsdelivr.net/npm/@xiee/utils/js/fold-details.min.js" defer></script>
```

By default, it folds code blocks that have classes, which means if you use
**knitr** / R Markdown / Quarto, only source code blocks will be folded. You can
customize its behavior via the `data-` attributes of the `<script>` tag that
loads `fold-details.js`:

-   `data-selector`: A selector to select elements to fold, e.g.,
    `"pre>code[class], table"`.

-   `data-open`: The initial status of the details elements, e.g., `"true"` to
    make them open initially (by default, they are closed).

-   `data-label`: The label of the details elements, e.g., `"View Details"`.

-   `data-tag-name`: In the labels, whether to display the tag names of elements
    that are folded, e.g., `"true"` to append `<CODE>` to the labels of details
    elements folding code blocks.

-   `data-button`:

    -   If `true`, a button (`<button id="toggle-all">`) to toggle all details
        on the page will be created if `data-parent` is also provided as a
        selector to find an existing parent element for the button, e.g.,
        `data-parent="body"` means to use the document body as the parent of the
        button (in this case, you may want to style the button with CSS
        `position: absolute;`). The button can be inserted into the parent
        element by different positions, which can be specified by
        `data-position`; see [the `insertAdjacentElement()`
        method](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement)
        for possible values (the default is `afterbegin`).

    -   Alternatively, you can specify `data-button` as a selector to select an
        existing element (not necessarily a button) on the page to act as the
        button. For example, if you have already gotten a
        `<span id="my-toggle">` on your page, you can specify
        `data-button="#my-toggle"`.

    -   By default, no button will be created or used.

-   `data-button-label`: The button label for closing all details, e.g.,
    `"Hide Details"`.

-   `data-button-label2`: The button label for opening all details. e.g.,
    `"Show Details"`.

## An example

I have loaded `fold-details.js` in this post via
`<script src="path/to/fold-details.js" data-open="true" data-button="#toggle-all">`
and created a button with the ID `toggle-all` below:

<script src="https://cdn.jsdelivr.net/npm/@xiee/utils/js/fold-details.min.js" data-open="true" data-button="#toggle-all" data-selector="pre>code[class],#TableOfContents~p:nth-last-of-type(3)" data-button-label="Show Details in This Post" data-button-label2="Hide Details in This Post" defer></script>

<p><button id="toggle-all">Toggle Details</button></p>

<style type="text/css">
#toggle-all {
  font-size: 1em;
  padding: .5em;
  margin: auto;
  display: block;
}
summary {
  font-style: italic;
  cursor: pointer;
  border-bottom: 1px solid var(--border-color);
}
details:last-of-type {
  border: 1px solid var(--border-color);
  padding: 1em;
  background-color: lightyellow;
}
</style>

You can click on the button to hide all details blocks in this post, which are
open initially due to the option `data-open="true"` that I specified for the
script.

I have also added a selector `#TableOfContents ~ p:nth-last-of-type(3)` to the
`data-selector` option, so that the third paragraph from the last after the
table of contents (i.e., this paragraph) will be moved into a details tag, too.

Personally I'm happy with this general solution. It is super lightweight yet
flexible, and can be applied to any element on any web page regardless of how
the page was generated (**blogdown**/Hugo, Quarto, Jekyll, and WordPress, etc.).
I hope you will find it useful. Happy folding!

![Keep folding](https://slides.yihui.org/gif/annoying-paper.gif)
