Building Pattern Libraries With Shadow DOM In Markdown

About The Author

Heydon Pickering (@heydonworks) has worked with The Paciello Group, The BBC, Smashing Magazine, and Bulb Energy as a designer, engineer, writer, editor, and … More about Heydon ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

Some people hate writing documentation, and others just hate writing. I happen to love writing; otherwise, you wouldn’t be reading this. It helps that I love writing because, as a design consultant offering professional guidance, writing is a big part of what I do. But I hate, hate, hate word processors. When writing technical web documentation (read: pattern libraries), word processors are not just disobedient, but inappropriate. Ideally, I want a mode of writing that allows me to include the components I’m documenting inline, and this isn’t possible unless the documentation itself is made of HTML, CSS and JavaScript. In this article, I’ll be sharing a method for easily including code demos in Markdown, with the help of shortcodes and shadow DOM encapsulation.

My typical workflow using a desktop word processor goes something like this:

  1. Select some text I want to copy to another part of the document.
  2. Note that the application has selected slightly more or less than I told it to.
  3. Try again.
  4. Give up and resolve to add the missing part (or remove the extra part) of my intended selection later.
  5. Copy and paste the selection.
  6. Note that the formatting of the pasted text is somehow different from the original.
  7. Try to find the styling preset that matches the original text.
  8. Try to apply the preset.
  9. Give up and apply the font family and size manually.
  10. Note that there is too much white space above the pasted text, and press “Backspace” to close the gap.
  11. Note that the text in question has elevated itself several lines at once, joined the heading text above it and adopted its styling.
  12. Ponder my mortality.

When writing technical web documentation (read: pattern libraries), word processors are not just disobedient, but inappropriate. Ideally, I want a mode of writing that allows me to include the components I’m documenting inline, and this isn’t possible unless the documentation itself is made of HTML, CSS, and JavaScript. In this article, I’ll be sharing a method for easily including code demos in Markdown, with the help of shortcodes and shadow DOM encapsulation.

An M, a down-arrow plus a dective hidden in the dark symbolizing Markdown and Shadown Dom

CSS And Markdown

Say what you will about CSS, but it’s certainly a more consistent and reliable typesetting tool than any WYSIWYG editor or word processor on the market. Why? Because there’s no high-level black-box algorithm that tries to second-guess what styles you really intended to go where. Instead, it’s very explicit: You define which elements take which styles in which circumstances, and it honors those rules.

The only trouble with CSS is that it requires you to write its counterpart, HTML. Even great lovers of HTML would likely concede that writing it manually is on the arduous side when you just want to produce prose content. This is where Markdown comes in. With its terse syntax and reduced feature set, it offers a mode of writing that is easy to learn but can still — once converted into HTML programmatically — harness CSS’ powerful and predictable typesetting features. There’s a reason why it has become the de facto format for static website generators and modern blogging platforms such as Ghost.

Where more complex, bespoke markup is required, most Markdown parsers will accept raw HTML in the input. However, the more one relies on complex markup, the less accessible one’s authoring system is to those who are less technical, or those short on time and patience. This is where shortcodes come in.

Shortcodes In Hugo

Hugo is a static site generator written in Go — a multi-purpose, compiled language developed at Google. Due to concurrency (and, no doubt, other low-level language features I don’t fully understand), Go makes Hugo a lightening-fast generator of static web content. This is one of the many reasons why Hugo has been chosen for the new version of Smashing Magazine.

Performance aside, it works in a similar fashion to the Ruby and Node.js-based generators with which you may already be familiar: Markdown plus meta data (YAML or TOML) processed via templates. Sara Soueidan has written an excellent primer on Hugo’s core functionality.

For me, Hugo’s killer feature is its implementation of shortcodes. Those coming from WordPress may already be familiar with the concept: a shortened syntax primarily used for including the complex embed codes of third-party services. For instance, WordPress includes a Vimeo shortcode that takes just the ID of the Vimeo video in question.


[vimeo 44633289]

The brackets signify that their content should be processed as a shortcode and expanded into the full HTML embed markup when the content is parsed.

Making use of Go template functions, Hugo provides an extremely simple API for creating custom shortcodes. For example, I have created a simple Codepen shortcode to include among my Markdown content:


Some Markdown content before the shortcode. Aliquam sodales rhoncus dui, sed congue velit semper ut. Class aptent taciti sociosqu ad litora torquent.

{{<codePen VpVNKW>}}

Some Markdown content after the shortcode. Nulla vel magna sit amet dui lobortis commodo vitae vel nulla sit amet ante hendrerit tempus.

Hugo automatically looks for a template named codePen.html in the shortcodes subfolder to parse the shortcode during compilation. My implementation looks like this:


{{ if .Site.Params.codePenUser }}
  <iframe height='300' scrolling='no' title="code demonstration with codePen" src='//codepen.io/{{ .Site.Params.codepenUser | lower }}/embed/{{ .Get 0 }}/?height=265&theme-id=dark&default-tab=result,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>
    <div>
      <a href="//codepen.io/{{ .Site.Params.codePenUser | lower }}/pen/{{ .Get 0 }}">See the demo on codePen</a>
    </div>
  </iframe>
{{ else }}
  <p class="site-error"><strong>Site error:</strong> The <code>codePenUser</code> param has not been set in <code>config.toml</code></p>
{{ end }}

To get a better idea of how the Go template package works, you’ll want to consult Hugo’s “Go Template Primer.” In the meantime, just note the following:

  • It’s pretty fugly but powerful nonetheless.
  • The {{ .Get 0 }} part is for retrieving the first (and, in this case, only) argument supplied — the Codepen ID. Hugo also supports named arguments, which are supplied like HTML attributes.
  • The . syntax refers to the current context. So, .Get 0 means “Get the first argument supplied for the current shortcode.”

In any case, I think shortcodes are the best thing since shortbread, and Hugo’s implementation for writing custom shortcodes is impressive. I should note from my research that it’s possible to use Jekyll includes to similar effect, but I find them less flexible and powerful.

Code Demos Without Third Parties

I have a lot of time for Codepen (and the other code playgrounds that are available), but there are inherent issues with including such content in a pattern library:

  • It uses an API so cannot be easily or efficiently made to work offline.
  • It doesn’t just represent the pattern or component; it is its own complex interface wrapped in its own branding. This creates unnecessary noise and distraction when the focus should be on the component.

For some time, I tried to embed component demos using my own iframes. I would point the iframe to a local file containing the demo as its own web page. By using iframes, I was able to encapsulate style and behavior without relying on a third party.

Unfortunately, iframes are rather unwieldy and difficult to resize dynamically. In terms of authoring complexity, it also entails maintaining separate files and having to link to them. I’d prefer to write my components in place, including just the code needed to make them work. I want to be able to write demos as I write their documentation.

The demo Shortcode

Fortunately, Hugo allows you to create shortcodes that include content between opening and closing shortcode tags. The content is available in the shortcode file using {{ .Inner }}. So, suppose I were to use a demo shortcode like this:


{{<demo>}}
    This is the content!
{{</demo>}}

“This is the content!” would be available as {{ .Inner }} in the demo.html template that parses it. This is a good starting point for supporting inline code demos, but I need to address encapsulation.

Style Encapsulation

When it comes to encapsulating styles, there are three things to worry about:

  • styles being inherited by the component from the parent page,
  • the parent page inheriting styles from the component,
  • styles being shared unintentionally between components.

One solution is to carefully manage CSS selectors so that there’s no overlap between components and between components and the page. This would mean using esoteric selectors per component, and it is not something I would be interested in having to consider when I could be writing terse, readable code. One of the advantages of iframes is that styles are encapsulated by default, so I could write button { background: blue } and be confident it would only apply inside the iframe.

A less intensive way to prevent components from inheriting styles from the page is to use the all property with the initial value on an elected parent element. I can set this element in the demo.html file:


<div class="demo">
    {{ .Inner }}
</div>

Then, I need to apply all: initial to instances of this element, which propagates to children of each instance.


.demo { all: initial }

The behavior of initial is quite… idiosyncratic. In practice, all of the affected elements go back to adopting just their user agent styles (like display: block for <h2> elements). However, the element to which it is applied — class=“demo” — needs to have certain user agent styles explicitly reinstated. In our case, this is just display: block, since class=“demo” is a <div>.


.demo { 
  all: initial;
  display: block;
}

Note: all is so far not supported in Microsoft Edge but is under consideration. Support is, otherwise, reassuringly broad. For our purposes, the revert value would be more robust and reliable but it is not yet supported anywhere.

Shadow DOM’ing The Shortcode

Using all: initial does not make our inline components completely immune to outside influence (specificity still applies), but we can be confident that styles are unset because we are dealing with the reserved demo class name. Mostly just inherited styles from low-specificity selectors such as html and body will be eliminated.

Nonetheless, this only deals with styles coming from the parent into components. To prevent styles written for components from affecting other parts of the page, we'll need to use shadow DOM to create an encapsulated subtree.

Imagine I want to document a styled `