Fractions

I’m on a quest to have nice-looking fractions for my recipes. How do these look on your device?

¼single character[U+00BC]
1⁄4fraction slash[U+0031, U+2044, U+0034]
1⁄4fixed fraction slash(HTML + CSS)

A dash in the rightmost column means that my fix didn’t activate on your device. If anything is wrong (if the fix is needed but didn’t activate, or if the fix produced weird results), please let me know!

Common fractions

Common fractions have their own Unicode codepoints. At time of writing, the full set is: ¼ ½ ¾ ⅐ ⅑ ⅒ ⅓ ⅔ ⅕ ⅖ ⅗ ⅘ ⅙ ⅚ ⅛ ⅜ ⅝ ⅞ ↉. But these have a few problems. They’re limited—no 1⁄16, for example—and many of them just look wrong. That’s because this font (Crimson Pro) only supplies glyphs for the first three of these fractions; the rest, if you can see them at all, are coming from fallback fonts chosen by your browser or OS.

my PC (Linux): my phone (Android): my iPad (iPadOS):
Common fractions as displayed by various devices

The fraction slash

It’s possible to write arbitrary fractions using the character U+2044 FRACTION SLASH together with ordinary decimal digits. The Unicode spec says that fractions written this way should be “displayed as a unit.”[1] For example, the sequence of characters 3 7 4 2 should be displayed as 37⁄42. However, this only happens if the font and the font rendering system both support this.

From my brief testing, fractions written with the fraction slash render correctly in:

But they don’t render correctly in:

These lists are obviously incomplete, but the takeaway for me was that I needed to do something different for iPadOS devices (and, I’m assuming, other Apple devices).

Fixing fractions for Apple devices

Goal: Surround each fraction on the page with HTML spans that can be styled with CSS, and, ideally, only do this when it’s actually needed.

I didn’t like the idea of explicitly marking fractions with special syntax when authoring, so instead I decided to scan for them later. I used the regular expression (\d+)\u2044(\d+) which matches a sequence of digits, the fraction slash, and another sequence of digits.

To scan each page for fractions, I had two options: I could scan when generating the pages (in my site generator code) or at page-load time using JavaScript. I opted for the latter because it seemed easier to implement, but I might revisit this decision.

Here’s the code that replaces the fractions within a particular element. It calls itself recursively for each child element. When it reaches a text node, if the node’s textContent contains a fraction slash, it replaces the text node with a new span element and replaces each contained fraction with its span-ified version.

function replaceFractions(element) {
  for (let child of element.childNodes) {
    switch (child.nodeType) {
      case Node.ELEMENT_NODE:
        replaceFractions(child);
        break;
      case Node.TEXT_NODE:
        if (child.textContent.includes("\u2044")) {
          let span = document.createElement("span");
          span.innerHTML = child.textContent
            // Encode the existing text as HTML
            .replaceAll("&", "&")
            .replaceAll("<", "&lt;")
            .replaceAll(">", "&gt;")
            // Surround fractions with spans
            .replaceAll(
              /(\d+)\u2044(\d+)/g,
              "<span class=replaced-fraction>" +
                "<span class=numerator>$1</span>" +
                "<span class=slash>\u2044</span>" +
                "<span class=denominator>$2</span>" +
              "</span>"
            );
          child.replaceWith(span);
        }
        break;
    }
  }
}

Note that it would be wrong to take the text node’s textContent and directly assign it to the new span’s innerHTML. The textContent is text, not HTML. It might contain characters like “<” which would gain undesired meaning if the text were parsed as HTML. So, it’s necessary to convert the text to HTML by replacing each & with &amp;, and so on. You can think of this as encoding the text into something that the HTML parser will decode back into the original text. Once I have HTML instead of text, I can replace each bare fraction with its HTML version.

Here’s the CSS I’m using to style these fractions. Note that using font-variant-position for superscript and subscript styles won’t look good unless the font specifically supports it.

.replaced-fraction {
  .numerator {
    font-variant-position: super;
  }
  .denominator {
    font-variant-position: sub;
  }
}

The final piece of the puzzle is to actually run this code over the whole page, but only on devices that need it. Ideally, I would be able to detect whether the browser supports fraction slash rendering, but as far as I can tell this isn’t possible. I settled instead for a simple test of navigator.platform. A Stack Overflow answer[2] suggested some potential values to check for, but I had to add mac to the list to get this to trigger on my iPad.

if (/^(ipad|iphone|ipod|mac)/i.test(navigator.platform)) {
  replaceFractions(document.body);
}

I expect there to be other devices that need the fix but aren’t covered by this check, but I’ll just have to wait until people report them to me. I suppose it might be possible to write some code to measure the width of a rendered fraction and decide based on that… but I’ll save that for later.

References

[1] Unicode 16.0.0 Core Spec, 6.2.9 Other Punctuation

[2] “What is the list of possible values for navigator.platform as of today?” on Stack Overflow