diff --git a/content/blog/sidenotes.md b/content/blog/sidenotes.md new file mode 100644 index 0000000..c329380 --- /dev/null +++ b/content/blog/sidenotes.md @@ -0,0 +1,178 @@ +--- +title: JavaScript-Free Sidenotes in Hugo +date: 2019-12-07T00:23:34-08:00 +tags: ["Website", "Hugo", "CSS"] +--- + +A friend recently showed me a website, the design of which I really liked: +Gwern Branwen's [personal website](https://www.gwern.net/index). In particular, +I found that __sidenotes__ were a feature that I didn't even know I needed. +A lot of my writing seems to use small parenthesized remarks (like this), which, +although it doesn't break the flow in a grammatical sense, lengthens the +sentence, and makes it harder to follow. Since I do my best to write content +to help explain stuff (like the [compiler series]({{ relref "00_compiler_intro.md" }})), +making sentences __more__ difficult to understand is a no-go. + +So, what do they look like? +{{< sidenote "right" "example-note" "Here's an example sidenote." >}} +This is this example note's content. +{{< /sidenote >}} +If you're on a mobile device, the content is hidden by default: there's no +"side" on which the note fits. In this case, you can click or tap the underlined +portion of the text, which is the part to which the sidenote is related or refers to. +Otherwise, the example sidenote should be visible +{{< sidenote "left" "left-note" "on the right side of the screen." >}} +Sidenotes can also appear on the left of the screen, to help prevent situations +in which there are too many sidenotes and not enough space. +{{< /sidenote >}} + +A major goal of mine in implementing these sidenotes was to avoid the use of JavaScript. +This is driven by my recent installation of uMatrix. uMatrix is an extension +that blocks JavaScript loaded from domains other than the one you're visiting. +To my frustration, a lot of the websites I commonly visit ended up broken: +[Canvas](https://github.com/instructure/canvas-lms), Discord, YouTube, and +Disquss all fail catastrophically when they aren't allowed to load dozens of scripts +from various sources. Out of spite, I want my site to work without any JavaScript, +and these notes are no exception. + +### Implementation +Some of this work has been inspired by +[this article](https://www.kooslooijesteijn.net/blog/semantic-sidenotes). +The first concern was not having to write raw HTML to add the side notes, +but this is fairly simple with Hugo's shortcodes: I write the HTML once in +a new `sidenote` shortcode, then call the shortcode from my posts. The next +issue is a matter of HTML standards. Markdown rendering generates `

` tags. +According the to spec, `

` tags cannot have a block element inside +them. When you _try_ to put a block element, such as `

` inside `

`, +the browser will automatically close the `

` tag, breaking the rest of the page. +So, even though conceptually (and visually) the content of the sidenote is a block, +{{< sidenote "right" "markdown-note" "it has to be inside an inline element." >}} +There's another consequence to this. Hugo implements Markdown inside shortcodes +by rendering the "inner" part of the shortcode, substituting the result into the +shortcode's definition, and then finally placing that into the final output. Since +the Markdown renderer wraps text in paragraphs, which are block elements, the +inside of the shortcode ends up with block elements. The same tag-closing issue +manifests, and the view ends up broken. So, Markdown cannot be used inside sidenotes. +{{< /sidenote >}} + +That's not too bad, overall. We end up with a shortcode definition as follows: +```HTML + + + + +{{ .Inner }} + + +``` +As Koos points out, "label" works as a semantic tag for the text that references +the sidenote. It also helps us with the checkbox +``, which we will examine later. Since it will receive its own style, +the inner content of the sidenote is wrapped in another ``. Let's +get started on styling the parts of a sidenote, beginning with the content: + +```SCSS +.sidenote-content { + display: block; + position: absolute; + width: $sidenote-width; + box-sizing: border-box; + margin-top: -1.5em; + + &.sidenote-right { + right: 0; + margin-right: -($sidenote-width + $sidenote-offset); + } + + // ... +} +``` + +As you can see from the sidenotes on this page, they are displayed as a block. +We start with that, then switch the sidenotes to be positioned absolutely, so that +we can place them exactly to the right of the content, and then some. We also +make sure that the box is sized __exactly__ the amount in `$sidenote-width`, by +ensuring that the border and padding are included in the size calculation +using `border-box`. We also hide the checkbox: + +```SCSS +.sidenote-checkbox { + display: none; +} +``` + +Finally, let's make one more adjustment to the sidenote and its label: when +you hover over one of them, the other will change its appearence slightly, +so that you can tell which note refers to which label. We can do so by detecting +hover of the parent element: + +```SCSS +.sidenote { + &:hover { + .sidenote-label { /* style for the label */ } + .sidenote-content { /* style for the sidenote */ } + } +} +``` + +### Hiding and Showing +So far, it's hard to imagine where JavaScript would come in. If you were +always looking at the page from a wide-screen machine, it wouldn't at all. +Unfortunately phones don't leave a lot of room for margins and sidenotes, so to +make sure that these notes are visible to mobile users, we want to show +them inline. Since the entire idea of sidenotes is to present more information +__without__ interrupting the main text, we don't want to plop something down +in the middle of the screen by default. So we hide sidenotes, and show them only +when their label is clicked. + +Gwern's site doesn't show the notes on mobile at all (when simulated using Firefox's +responsive design mode), and Koos uses JavaScript to toggle the sidenote text. We will +go another route. + +This is where the checkbox `` comes in. When the `` checkbox is +checked, we show the sidenote text, as a block, in the middle of the page. When +it is not checked, we keep it hidden. Of course, keeping a checkbox in the middle +of the page is not pretty, so we keep it hidden. Rather than clicking the checkbox +directly, +{{< sidenote "right" "accessibility-note" "the users can click the text that refers to the sidenote," >}} +I'm not sure about the accessibility of such an arrangement. The label is semantic, sure, but +the checkbox is more sketchy. Take this design with a grain of salt. +{{< /sidenote >}} which happens +to also be a label for the checkbox input. Clicking the label toggles the checkbox, +and with it the display of the sidenote. We can use the following CSS to +get that to work: + +```SCSS +.sidenote-content { + // ... + @media screen and + (max-width: $container-width + 2 * ($sidenote-width + 2 * $sidenote-offset)) { + position: static; + margin-top: 10px; + margin-bottom: 10px; + width: 100%; + display: none; + + .sidenote-checkbox:checked ~ & { + display: block; + } + + &.sidenote-right { + margin-right: 0px; + } + + // ... + } + // ... +} +``` + +We put the position back to `static`, and add margins on the top and bottom of the node. +We keep the `display` to `none`, unless the checkbox contained in the sidenote span +is checked. Finally, we reset the margin we created earlier, since we're not moving this +note anywhere. + +### Conclusion +Here, we've implemented sidenotes in Hugo with zero JavaScript. They work well on +both mobile and desktop devices, though their accessibility is, at present, +somewhat uncertain.