Collapsing content in browsers, feed readers & beyond

Team blog: Developers

The world is pretty simple when you don’t have to worry about content re-use, accessibility, internationalization, and similar concerns. But the moment you prioritize those issues, even relatively simple features become complex.

I wanted to write a review with spoilers, so in the spirit of developing features the moment I need them as an active user of the site, I put something together that extends the markdown parser we use. It works as follows:

::: spoiler
Some content with spoilers
:::

produces:

Warning: The text below contains spoilers.

Some content with spoilers

::: nsfw
Some NSFW content
:::

produces:

Warning: The content below may not be safe for work (NSFW).

Some NSFW content

::: warning Here there be dragons
Some dangerous content
:::

produces:

Here there be dragons

Some dangerous content

The choice of ::: isn’t arbitrary – it’s used by the markdown-it-container plugin which enables block-level extension to markdown-it that follow this pattern. We may use it in future for similar purposes with other block names.

Indeed, the help page for that package lists spoiler warnings as an example. But how you do them in a manner that 1) degrades gracefully, 2) is keyboard-accessible, 3) works in RSS readers and reviews obtained via API calls and inserted into someone else’s webpage?

The easiest choice would be to just expand the content if JavaScript is disabled, and otherwise collapse/expand it via JS. That may seem like a reasonable choice but it’s important to note that the use case 3) will often be affected – i.e., external content re-use will often show the collapsed content expanded, which is not desirable.

If you look around, you’ll find that there are a number of pure-CSS hacks to collapse/expand content. They work, but they rely on problematic choices, such as hidden checkbox inputs that toggle the collapse/expand state of content. Those are problematic because they mess with keyboard-accessibility.

In addition, CSS itself is not a reliable delivery method for content re-users. Pseudo-elements like :checked cannot be used in inline styles, and scoped stylesheets have been removed from the WHATWG standards and are supported only by Firefox.

Enter the <details> element. It is precisely intended to collapse or otherwise conceal some content by default, and to expand it on demand. Unfortunately, it’s not widely supported yet, but in browsers that do support it, this gets us a nice basic collapse/expand feature that works even without CSS or JavaScript! And where it’s not supported, the whole content is just displayed, not hidden.

But the look & feel is determined by the browser. So I added a bit of CSS and JavaScript that effectively overrides the built-in look and behavior when JavaScript is available (including the addition of a slide-in and slide-out animation) – and implicitly shims the feature for browsers that don’t have it at all.

That leaves only the case where 1) JavaScript is disabled, 2) the <details> element is not supported. In that case, the text will just be expanded by default. I tested this on a couple of mobile browsers and RSS readers, and it worked as expected.

Now what about i18n? markdown-ititself doesn’t have the notion of content language, but it has an environment that can be configured for each call of the render function that converts markdown into HTML. We pass the language into that environment, and our plugin picks the appropriate message to insert. For client-side live previews, we expose the necessary UI messages as JavaScript variables.

The message is ultimately stored as part of the content – which seems reasonable, given that it can be customized as in the “Here be dragons” example. But it has the side effect that if you switch the language of this post to German, the English warnings will still be English. A German review would have German messages. I think that’s an acceptable trade-off for UI text that really is part of the content. In any case, we retain the source code of all content, so we can re-render it in a different way in future if we want to.

Let me know if anything’s not working as it should (or file a bug). Check out the commit for the nitty gritty of how this was done.