Qubyte Codes

Controlling ruby annotation positioning and appearance with pure CSS and a select box

Published

ふりがな:

The :has() CSS pseudo-class opens up all sorts of possibilities. I wanted to see if it could simplify how I handle the ruby text (annotations above or below text to help with reading) in my Japanese notes. It works (in Safari and Chrome at least, and hopefully Firefox soon)!

Demonstration time. Below the date stamp in the header above you'll see "ふりがな". To the right is a word. Click or tap on the word to select the positioning of the annotations in the Japanese text below, or hide them. This works even with JS turned off.

この(ぶん)にはふりがながあります。

Previously I had to use some JS to achieve this. Pretty simple CSS can be used to target and style ruby text (although Safari lags behind the standard). I used JS to append and update a class to the body to set the mode desired. The markup for ruby text is managed using my own extensions to Markdown.

The replacement CSS looks like this (there's a little more to handle the quirks of <mark> elements which I omit here):

/* Select <body> when an element with class furigana-position has an <option>
 * with value "over" and that option is in the checked state.
 */
body:has(.furigana-position option[value="over"]:checked) {
  ruby-position: over;
  -webkit-ruby-position: before;
}

/* Select <body> when an element with class furigana-position has an <option>
 * with value "under" and that option is in the checked state.
 */
body:has(.furigana-position option[value="under"]:checked) {
  ruby-position: under;
  -webkit-ruby-position: after;
}

/* Select <rt> and <rp> elements in the <body> when an element with class
 * furigana-position has an <option> with value "off" and that option is in the
 * checked state.
 */
body:has(.furigana-position option[value="off"]:checked) :is(rt, rp) {
  display: none;
}

I have kept a little JS to persist this state to local storage, as it was in the earlier version of this feature. Now that only one place in the DOM needs to be synchronized (it was two before; the option and the class on the body element) the JS has become much simpler. Most importantly, it's a progressive enhancement of sorts. It's not required for the positioning feature to work, just for the state to be saved.

const select = document.querySelector('.furigana-position');

select.value = localStorage.getItem('ruby-position') || 'over';
select.onchange = e => localStorage.setItem('ruby-position', e.target.value);

Finally, a note on why I have this feature. For learners, it can be helpful to show and hide the annotations depending on what you're reading and why. It can also be helpful, especially on a tablet, to position furigana below the text it annotates, so it can be hidden with a sheet of paper as you read.


Backlinks


Feel like sharing or responding?

Comments or corrections? Toot to me!

Edit page.