Build-time rendering
Grigson charts can be pre-rendered at build time using Declarative Shadow DOM (DSD). The chart is fully visible in the browser before any JavaScript runs — useful for static sites, SSR frameworks, and environments where JS may be disabled or slow to load.
Live demo
The chart below was rendered at Eleventy build time. Try disabling JavaScript in DevTools and reloading — it will still render correctly.
Blues in F
B♭ major
Head
How it works
An Eleventy transform post-processes every .html output file. It finds each
<grigson-chart> element and calls parseSong and
HtmlRenderer.render() from the grigson package at build time (Node.js).
The resulting HTML string is placed inside a <template shadowrootmode="open">
injected as the first child of the element:
<grigson-chart normalise>
<template shadowrootmode="open">
<style>:host{display:block;container-type:inline-size}</style>
<style>/* component CSS */</style>
<!-- pre-rendered chart HTML -->
</template>
<template><!-- original .chart source --></template>
</grigson-chart>
When the browser parses this HTML it immediately attaches the shadow root — no script required.
When JavaScript loads, <grigson-chart> upgrades and detects the existing
shadow root, adopting it rather than re-rendering. The element only re-renders if an attribute
changes or the source template is mutated.
The :host{display:block;container-type:inline-size} rule is included in the DSD
template even though <grigson-chart> also sets it via JavaScript. Without it,
@container queries inside the shadow root would not fire until after JS upgrades the
element — breaking responsive layouts when JS is disabled or slow to load.
Transform registration
Install eleventy-plugin-grigson and add it to your .eleventy.js.
The transform runs automatically on every page — no per-element markup changes are needed.
import grigsonPlugin from 'eleventy-plugin-grigson';
eleventyConfig.addPlugin(grigsonPlugin);
Font note
getRendererFontFaceCSS() returns @font-face rules with the notation
fonts embedded as data URIs. These are emitted into the main document (not the shadow
root) so that browsers reliably load the unicode-range sub-faces — browsers have known
limitations with multiple @font-face rules sharing a family name inside shadow roots.
Because the declarations use data URIs, no network requests are made. The component styles go
inside the shadow root where they are properly scoped.