1 changed files with 178 additions and 0 deletions
@ -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 `<p>` tags. |
||||
According the to spec, `<p>` tags cannot have a block element inside |
||||
them. When you _try_ to put a block element, such as `<div>` inside `<p>`, |
||||
the browser will automatically close the `<p>` 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 |
||||
<span class="sidenote"> |
||||
<label class="sidenote-label" for="{{ .Get 1 }}">{{ .Get 2 }}</label> |
||||
<input class="sidenote-checkbox" type="checkbox" id="{{ .Get 1 }}"></input> |
||||
<span class="sidenote-content sidenote-{{ .Get 0 }}"> |
||||
{{ .Inner }} |
||||
</span> |
||||
</span> |
||||
``` |
||||
As Koos points out, "label" works as a semantic tag for the text that references |
||||
the sidenote. It also helps us with the checkbox |
||||
`<input>`, which we will examine later. Since it will receive its own style, |
||||
the inner content of the sidenote is wrapped in another `<span>`. 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 `<input>` comes in. When the `<input>` 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. |
Loading…
Reference in new issue