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 `button` element. I'd like to be able to simply write something like the following, without fear that the button element selector will apply to button elements in the pattern library itself or in other components in the same library page.


{{<demo>}}
<button>My button</button>
<style>
button {
    background: blue;
    padding: 0.5rem 1rem;
    text-transform: uppercase;
}
</style>
{{</demo>}}

The trick is to take the {{ .Inner }} part of the shortcode template and include it as the innerHTML of a new ShadowRoot. I might implement this like so:


{{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }}
<div class="demo" id="demo-{{ $uniq }}"></div>
<script>
    (function() {
        var root = document.getElementById('demo-{{ $uniq }}');
        root.attachShadow({mode: 'open'});
        root.innerHTML = '{{ .Inner }}';
    })();
</script>
  • $uniq is set as a variable to identify the component container. It pipes in some Go template functions to create a unique string… hopefully(!) — this isn't a bulletproof method; it's just for illustration.
  • root.attachShadow makes the component container a shadow DOM host.
  • I populate the innerHTML of the ShadowRoot using {{ .Inner }}, which includes the now-encapsulated CSS.

Permitting JavaScript Behavior

I'd also like to include JavaScript behavior in my components. At first, I thought this would be easy; unfortunately, JavaScript inserted via innerHTML is not parsed or executed. This can be solved by importing from the content of a `template` element. I amended my implementation accordingly.


{{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }}
<div class="demo" id="demo-{{ $uniq }}"></div>
<template id="template-{{ $uniq }}">
    {{ .Inner }}
</template>
<script>
    (function() {
        var root = document.getElementById('demo-{{ $uniq }}');
        root.attachShadow({mode: 'open'});
        var template = document.getElementById('template-{{ $uniq }}');
        root.shadowRoot.appendChild(document.importNode(template.content, true));
    })();
</script>

Now, I'm able to include an inline demo of, say, a working toggle button:


{{<demo>}}
<button>My button</button>
<style>
button {
    background: blue;
    padding: 0.5rem 1rem;
    text-transform: uppercase;
}

[aria-pressed="true"] {
    box-shadow: inset 0 0 5px #000;
}
</style>
<script>
var toggle = document.querySelector('[aria-pressed]');

toggle.addEventListener('click', (e) => {
  let pressed = e.target.getAttribute('aria-pressed') === 'true';
  e.target.setAttribute('aria-pressed', !pressed);
});
</script>
{{</demo>}}

Note: I have written in depth about toggle buttons and accessibility for Inclusive Components.

JavaScript Encapsulation

JavaScript is, to my surprise, not encapsulated automatically like CSS is in shadow DOM. That is, if there was another [aria-pressed] button in the parent page before this component's example, then document.querySelector would target that instead.

What I need is an equivalent to document for just the demo's subtree. This is definable, albeit quite verbosely:


document.getElementById('demo-{{ $uniq }}').shadowRoot;

I didn't want to have to write this expression whenever I had to target elements inside demo containers. So, I came up with a hack whereby I assigned the expression to a local demo variable and prefixed scripts supplied via the shortcode with this assignment:


if (script) {
  script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()`
}
root.shadowRoot.appendChild(document.importNode(template.content, true));

With this in place, demo becomes the equivalent of document for any component subtrees, and I can use demo.querySelector to easily target my toggle button.


var toggle = demo.querySelector('[aria-pressed]');

Note that I have enclosed the demo's script contents in an immediately invoked function expression (IIFE), so that the demo variable — and all proceeding variables used for the component — are not in the global scope. This way, demo can be used in any shortcode's script but will only refer to the shortcode in hand.

Where ECMAScript6 is available, it's possible to achieve localization using "block scoping," with just braces enclosing let or const statements. However, all other definitions within the block would have to use let or const (eschewing var) as well.


{
    let demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot;
    // Author script injected here
}

Shadow DOM Support

Of course, all of the above is only possible where shadow DOM version 1 is supported. Chrome, Safari, Opera and Android all look pretty good, but Firefox and Microsoft browsers are problematic. It is possible to feature-detect support and provide an error message where attachShadow is not available:


if (document.head.attachShadow) {
  // Do shadow DOM stuff here
} else {
  root.innerHTML = 'Shadow DOM is needed to display encapsulated demos. The browser does not have an issue with the demo code itself';
}

Or you can include Shady DOM and the Shady CSS extension, which means a somewhat large dependency (60 KB+) and a different API. Rob Dodson was kind enough to provide me with a basic demo, which I'm happy to share to help you get started.

Captions For Components

With the basic inline demo functionality in place, quickly writing working demos inline with their documentation is mercifully straightforward. This affords us the luxury of being able to ask questions like, "What if I want to provide a caption to label the demo?" This is perfectly possible already since — as previously noted — Markdown supports raw HTML.


<figure role="group" aria-labelledby="caption-button">
    {{<demo>}}
    <button>My button</button>
    <style>
    button {
        background: blue;
        padding: 0.5rem 1rem;
        text-transform: uppercase;
    }
    </style>
    {{</demo>}}
    <figcaption id="caption-button">A standard button</figcaption>
</figure>

However, the only new part of this amended structure is the wording of the caption itself. Better to provide a simple interface for supplying it to the output, saving my future self — and anyone else using the shortcode — time and effort and reducing the risk of coding typos. This is possible by supplying a named parameter to the shortcode — in this case, simply named caption:


{{<demo caption="A standard button">}}
    ... demo contents here...
{{</demo>}}

Named parameters are accessible in the template like {{ .Get "caption" }}, which is simple enough. I want the caption and, therefore, the surrounding figure and figcaption to be optional. Using if clauses, I can supply the relevant content only where the shortcode provides a caption argument:


{{ if .Get "caption" }}
    <figcaption>{{ .Get "caption" }}</figcaption>
{{ end }}

Here's how the full demo.html template now looks (admittedly, it's a bit of a mess, but it does the trick):


{{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }}
{{ if .Get "caption" }}
<figure role="group" aria-labelledby="caption-{{ $uniq }}">
{{ end }}
    <div class="demo" id="demo-{{ $uniq }}"></div>
    {{ if .Get "caption" }}
        <figcaption id="caption-{{ $uniq }}">{{ .Get "caption" }}</figcaption>
    {{ end }}
{{ if .Get "caption" }}
</figure>
{{ end }}
<template id="template-{{ $uniq }}">
    {{ .Inner }}
</template>
<script>
  (function() {
      var root = document.getElementById('demo-{{ $uniq }}');
      root.attachShadow({mode: 'open'});
      var template = document.getElementById('template-{{ $uniq }}');
      var script = template.content.querySelector('script');
      if (script) {
          script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()`
       }
       root.shadowRoot.appendChild(document.importNode(template.content, true));
  })();
</script>

One last note: Should I want to support markdown syntax in the caption value, I can pipe it through Hugo's markdownify function. This way, the author is able to supply markdown (and HTML) but is not forced to do either.


{{ .Get "caption" | markdownify }}

Conclusion

For its performance and its many excellent features, Hugo is currently a comfortable fit for me when it comes to static site generation. But the inclusion of shortcodes is what I find most compelling. In this case, I was able to create a simple interface for a documentation issue that I've been trying to solve for some time.

As in web components, a lot of markup complexity (sometimes exacerbated by adjusting for accessibility) can be hidden behind shortcodes. In this case, I'm referring to my inclusion of role="group" and the aria-labelledby relationship, which provides a better supported "group label" to the figure — not things that anyone relishes coding more than once, especially where unique attribute values need to be considered in each instance.

I believe shortcodes are to Markdown and content what web components are to HTML and functionality: a way to make authorship easier, more reliable and more consistent. I look forward to further evolution in this curious little field of the web.

Resources