Pretty <ruby> for CJK languages
Tags: css, html, typography, design
Published: 2023-06-04 16:39:36

Recently, I've been experimenting with East Asian typography and with creating print-quality output using HTML and CSS. However, it didn't take long and I noticed something: _rubies are ugly!_ I haven't really found articles about the topic in English, so here's my attempt at one.


## Wait, what?

If you're not familiar with the name “ruby”, they are small characters above the text, usually for providing pronunciation hints. For example, they can show _furigana_ for Japanese, or _bopomofo_ for Chinese, but also Latin letters as well.

The ruby element consists of a ruby _base_, and the ruby _text_, that most often sits on its top:

![Ruby base and ruby text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nla8g5g6rwhlmfj688pn.png)

In HTML, we can use the `<ruby>` tag to define a whole group, in which `<rb>`[^rb-deprecated] defines the ruby _base_, and `<rt>` the ruby _text_.[^ruby-table] (Spaces added below for readability.)

```html
<ruby lang="ja"> <rb>東京</rb>  <rt>とうきょう</rt> </ruby>
<ruby lang="zh"> <rb>北京</rb>  <rt>Běijīng</rt> </ruby>
<ruby lang="vi"> <rb>河內</rb>  <rt>Hà Nội</rt> </ruby>
```


## The naive approach

Now what happens when the ruby text is wider than the ruby base? By default, `<ruby>` acts sort of like a single block of text:

![Default ruby style](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4l6x27y35lru9x0iryky.png)

In Japanese typography, however, it often looks more pleasant to spread the text over the neighboring characters, without any spacing[^w3-jp]:

![Pretty ruby style](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5p309826onrfw1w0lhca.png)

This could be solved with a little CSS:

- take the ruby text out of the regular text flow with `position: absolute`, then
- align it horizontally to the center of its parent, with something like `left: 50%; transform: translateX(-50%)`, and
- move it to the top with `bottom: 100%`.

```css
ruby {
    position: relative;
}
ruby rt {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    bottom: 100%;
}
```

And this works perfectly fine in Firefox, producing the earlier image.

Unfortunately, the implementation in Chrome and Safari [lags behind][ruby-dashboard] at the moment, and the `position` attribute [does not seem to work at all][ruby-position-bug] there.


## An alternative

If we cannot use the built-in `<rt>` element, we could try to replace it with the CSS pseudo-element `::before`. If, instead of

```html
<ruby>東京<rt>とうきょう</rt></ruby>
```

we write

```html
<ruby data-rt="とうきょう">東京</ruby>
```

this stores the ruby text as a custom attribute, which we can access from CSS:

```css
ruby[data-rt]::before {
    content: attr(data-rt);
}
```

and, in addition to the very first styling attempt, to make it look like the original `<rt>` tag:

```css
ruby[data-rt]::before {
    font-size: .5em;
    line-height: 1;
}
```

The result looks visually the same as our first attempt!


## Sidenotes

The above approach should work for most cases, including vertical writing. Corner cases might appear however if you try to build on top of if. As usual, most of these can be solved with a hint JavaScript code.

- **Shorter text:** You might also want to spread out the characters if the ruby base is wider than the ruby text. An approach for that is to split the text to individual characters with JavaScript, then spread them with flexbox styling (`justify-content: space-around` for example happens to match the Japanese styling specification). However, you cannot target CSS pseudo-elements with JS, so you might need to manually construct a child element for your `<ruby>`es.
- **Body overflow:** If you want to be very precise, you might want to handle ruby texts flowing out of the body text area, i.e. make the text align to one of the sides.
- **Overlaps:** The ruby texts might overlap or touch, though in practice the chance for that shouldn't be too high. If this becomes an issue, you can detect such cases using `getBoundingClientRect()`, and add some padding if necessary.
- **Compound words:** If you want to use multiple ruby texts in one single ruby element (eg. per-character pronunciation), you might need to split the ruby elements. If the ruby base can break eg. at line ends, the ruby texts should probably follow that too.

You might also need to do some preprocessing, based on your source text:

- **From HTML:** If your text is in HTML and already uses `<ruby>` and `<rt>`, you can use JavaScript to query all ruby elements, and move the text content from the `<rt>` into the data property of the `<ruby>`.
- **From Markdown:** If your text is in Markdown or similar, a common ruby pattern is like this: `{東京|とう|きょう}`, that is, `{base|text1|text2|...|textN}`, where each `text` segment is the reading of a base character.
- **From plain text:** If you have plain text, where the reading is next to the word (eg. `東京《とうきょう》`), you can always just replace them with a regular expression, as long as the writing is consistent.

---

A nicely typeset page pleases the eye, and often requires just a tiny bit of additional care. If you happen to work with East Asian text a lot, I hope this will help to make your content look even better.



[ruby-dashboard]: https://wpt.fyi/results/css/css-ruby
[ruby-position-bug]: https://bugs.chromium.org/p/chromium/issues/detail?id=847743

[^w3-jp]: See https://www.w3.org/TR/jlreq/?lang=en for the whole specification.
[^rb-deprecated]: The `<rb>` tag is actually unnecessary now (you can directly write the text there), but in this example shows the element structure more clearly.
[^ruby-table]: For a long time, `<ruby>` wasn't well supported, so people also used “creative” solutions, like tables for alignment. You might still run into those on some sites.