Compare commits
2 Commits
b0529a9124
...
f75a47e273
Author | SHA1 | Date | |
---|---|---|---|
f75a47e273 | |||
9eae560cae |
178
content/blog/sidenotes.md
Normal file
178
content/blog/sidenotes.md
Normal 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.
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user