Compare commits

..

2 Commits

Author SHA1 Message Date
f75a47e273 Add post about sidenotes 2019-12-08 23:47:52 -08:00
9eae560cae Make sidenotes mobile-friendly 2019-12-07 00:16:59 -08:00
3 changed files with 217 additions and 13 deletions

178
content/blog/sidenotes.md Normal file
View File

@ -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.

View File

@ -4,16 +4,6 @@ $sidenote-width: 350px;
$sidenote-offset: 15px;
.sidenote {
&.right .sidenote-content {
right: 0;
margin-right: -($sidenote-width + $sidenote-offset);
}
&.left .sidenote-content {
left: 0;
margin-left: -($sidenote-width + $sidenote-offset);
}
&:hover {
.sidenote-label {
background-color: $primary-color;
@ -32,12 +22,47 @@ $sidenote-offset: 15px;
border-bottom: 2px solid $primary-color;
}
.sidenote-checkbox {
display: none;
}
.sidenote-content {
display: block;
position: absolute;
width: $sidenote-width;
margin-top: -1.5em;
&.sidenote-right {
right: 0;
margin-right: -($sidenote-width + $sidenote-offset);
}
&.sidenote-left {
left: 0;
margin-left: -($sidenote-width + $sidenote-offset);
}
@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-left {
margin-left: 0px;
}
&.sidenote-right {
margin-right: 0px;
}
}
@include bordered-block;
padding: 10px;
box-sizing: border-box;

View File

@ -1,6 +1,7 @@
<span class="sidenote {{ .Get 0 }}">
<label class="sidenote-label">{{ .Get 2 }}</label>
<span class="sidenote-content">
<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>