Code Folding with 6 Lines of Vanilla JavaScript

Or how to fold any element on a web page using the <details> tag

Yihui Xie 2023-09-19

Do you know what the top requested feature of blogdown has been in the past six years? Code folding. 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 (thanks, Emi Tanaka, a cool hacker 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, 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.

On May 31st this year, Xiangyun said that the only missing feature of blogdown / Hugo sites he would want was code folding. Unfortunately I went 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.

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

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.

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:

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

Then add a click event to it via JavaScript:

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 (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 (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 and you can just load it on your page:

<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:

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:

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


  1. You may not like details to “snap” on toggle, but prefer a smooth transition. That can be achieved by more JS code (which I forked and tweaked from Louis Hoebregts’s work, but honestly, I do not understand it). ↩︎