Kyle Fontenot

Photo of Kyle Fontenot
description All blog articles

Bionic Reading

July 5, 2022

ReadabilityBionicWebDev Tools

Bionic reading is a really interesting idea of improving readability –especially with dyslexic readers– by emphasizing the beginning letter(s) of each word of a paragraph.

Screenshot of an implementation of bionic reading

It’s a fascinating concept that can be used for many reading uses, and it seems like something that many people may prefer. The official website advertises an official API and a few other tools for enabling and mutating blocks of text into a “bionic”-style, but I wanted to explore implementing something like this simply with Javascript. I searched NPM for something similar, but with no solid results.

So I made a mini library for myself.

Obstacles

My qualifications for it was to create a drop and go function or class like most JS libraries that would affect all paragraphs(or specified selector or class) of text on the HTML document. There’s quite a few obstacles to consider in this particular idea:

  • How would I be able to isolate only the first letters of each word while still retaining the inline, static dimensions of the original paragraph?
  • How would I stylize a way for the emphasized letters to be exactly the same font, almost like an outline, that is 1 pixel at most wider than the original font?
  • Do I implement this with CSS or some way with Javascript that will perfectly outline a font shape?
  • Are pseudo elements an option?
  • If I were to use CSS, what would be the best property to use?
    • text-shadow?
    • font-weight?
    • transform: translate()?
  • How would web crawlers understand?

My Solution

I came to my solution that uses CSS’s text-shadow and the Shadow DOM to create a distinction between the original light DOM content and the specifically styled bionic node attached as a child. I chose text-shadow as a default because it is the most likely to not be used and changed externally (since the shadow DOM is open for flexibility)., and becausetext-shadow also keeps the exact fidelity of the font used. Also text-shadow doesn’t rely on the availability of a bolder font weight loaded.

This would still show indexable to web crawlers, but because of the requirement on keeping the added outline exactly the same as the computed font, it would be much harder, if not impossible to add in a shape without adding an HTML node. The shadow DOM at least creates the distinction, readable to devs, while still unaffecting the “light DOM” node’s indexability.

class Bionic extends HTMLParagraphElement {
  constructor(elem){
    super();
    const root = document.createElement('bionic-reading');
    const shadow = root.attachShadow({mode: 'open'});
    const paragraph = document.createElement('bionic-reading');

    const elemStyle = window.getComputedStyle(elem)
    paragraph.style = elemStyle;
    paragraph.innerHTML = elem.innerHTML;

    root.classList.add('bionic');
    paragraph.classList.add('bionic');
    const style = document.createElement('style');
    style.textContent = `
      .bionic {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
      }
    `
    elem.style.position = "relative"
    root.style.color = "transparent";
    paragraph.style.margin = "0";

    paragraph.innerHTML = paragraph.innerHTML.split(' ').map(word => {
      return `<span style="text-shadow:-2px 0 0 ${elemStyle.color};">${word[0]}</span>` + word.substring(1)
    }).join(' ')
    shadow.appendChild(style)
    shadow.appendChild(paragraph)
    this.result = root
  }
}

function bionic(selector = 'p'){
  customElements.define('bionic-reading', Bionic, { extends: selector });
  document.querySelectorAll(selector).forEach(elem => {
    let bionic = new Bionic(elem);
    elem.parentNode.insertBefore(bionic.result, elem.nextSibling);
    elem.appendChild(bionic.result);
  })
}

bionic()

Essentially, it creates a child Shadow DOM, adds in an absolutely positioned clone of the light DOM node, and then injects a span that creates a text-shadow for every first letter of each word. Styles from the light DOM are inherited for the sake of keeping text-based styles like letter-spacing and color consist so that the emphasized letters line up perfectly. It requires the light DOM node to be relatively positioned (which shouldn’t make a difference from static in how we normally use semantic paragraphs anyways).

If I expand this tool potentially into an NPM package, I can easily allow for a few option parameters such as the property value that changes the CSS property used ('text-shadow' | 'transform' | 'filter' | font-weight ), heaviness, letter padding, HTML and/or class selector.

Let me know what you think!