Building a Text Annotation Component with React

In this post we're going to have a look at how to build a component to annotate text. This is probably useful for linguistic analysis, I primarily built it for song lyrics and to do some communication breakdown of some messages I get that are wrong on so many levels, that I need visual aid to break them down.

Genius Like Song Annotation

One of the examples I thought of when building this in my head was genius.com, they have a community of people doing interpretations of lines in songs, which is a bit different than having to know every reference, but you can submit an interpretation or something you know of a specific passage, which is interesting.

screenshot of genius dot com

Theoretical Requirements of Text Annotation

  1. I want to be able to highlight words and sentences and write a bullet point about that specific piece of text below the actual text.
  2. Both words and bullet point need to be coloured in the same colour.
  3. Different words should be able to be referenced by the same bullet point, for example "The egg is in the basket", both egg and basket should have the annotation: noun

This is turning out to be a bit like annotated text in a school book for learning languages, but that's not too bad. School books aim to clarify and visualise knowledge for 100% of the seeing population, so I guess I can't get a more versatile role model than that.

The search for a colour palette went down a bit of a rabbit hole, so I'll just explain my colour choices here instead of later:

.breakdown {
	--color-opacity: 0.666; /* heh */
	--color-hl-fallback: hsla(56, 85%, 60%, var(--color-opacity));
	--color-hl-01: hsla(331, 72%, 51%, var(--color-opacity));
	--color-hl-02: hsla(223, 100%, 70%, var(--color-opacity));
	--color-hl-03: hsla(251, 83%, 65%, var(--color-opacity));
	--color-hl-04: hsla(23, 100%, 50%, var(--color-opacity));
	--color-hl-05: hsla(41, 100%, 50%, var(--color-opacity));
}

These are the colours I use and they are slightly brightened colours from the colour blindness friendly palettes collected here: davidmathlogic.com/colorblind/ which are super great. Also I'm using the hsl(a) colour space to be able to darken, brighten, play with the opacity.

The React <TextBreakdown/> Component

I needed a react component, since I'm going to use this in the context of next.js and displaying it in MDX posts for this blog, so I wanted to go for something that is fairly vanilla HTML in the elements that are passed. We're going to use the fantastic metal history song Metal Crüe (2006) by Sabaton.

The passage we want to break down a little is the chorus:

When the priest killed a maiden in the metal church Armored saints and warlocks watched the slaughter Rage of the slayer forced the pretty maids To kiss the Queen in crimson glory

Alright we have lots of metal bands we need to reference.

I started by writing the component by having the data just as variables before the render, but here's how the component works:

<TextBreakdown notes={[<>Hello, I'm a note</>]}>
// children go here
<p>Let's see how these <mark data-note-index="0">annotated</mark> things look!</p>
</TextBreakdown>

We're using the <mark> tag here, which is an HTML tag meant for highlighting

So both the prop notes and the children of the component are react nodes aka HTML elements. Technically these are not the same, but for all we care they are when writing content.

Now we just need to write a component that receives the props and children to do our bidding and satisfy our highlighting needs:

// TextBreakdown.tsx
import React from "react";

import styles from './TextBreakdown.module.css';

const TextBreakdown = ({ notes, children }) => {
	const notesList = notes ? notes.map((note, index) => <li key={index} data-note-index={index}>{note}</li >) : null;

	return (<div className={styles['breakdown']}>
		<div className={styles['breakdown-text']}>
			{children}
		</div>
		<ul>
			{notesList}
		</ul>
	</div>)
}

export default TextBreakdown;

The important bit about this file is only this bit:

const notesList = notes ? notes.map((note, index) => <li key={index} data-note-index={index}>{note}</li >) : null;

You could also just pass strings to the notes prop, however I wanted hyperlinks to work and other HTML elements like images. Even better for my workflow would possibly be to pass markdown strings, but that would escape the scope of this for now.

Here we assign the same data-note-index to the notes list that we already use in our content. Then we move on to styling:

.breakdown {
	--color-opacity: 0.666;
	--color-hl-fallback: hsla(56, 85%, 60%, var(--color-opacity));
	--color-hl-01: hsla(331, 72%, 51%, var(--color-opacity));
  /* other colours mentioned above */
}

.breakdown mark {
	padding: 3px;
	background-color: var(--color-hl-fallback);
}

.breakdown mark[data-note-index="0"] {
	background-color: var(--color-hl-01);
}

.breakdown mark[data-note-index="1"] {
	background-color: var(--color-hl-02);
}

/* and so on until index 4 */

.breakdown li {
	margin-top: 10px;
	padding: 5px;
	background-color: var(--color-hl-fallback);
}

.breakdown li[data-note-index="0"] {
	background-color: var(--color-hl-01);
}

/* and so on until index 4 */

We're targeting [data-note-index="0"] and assign the colour for either the mark background colour or the list item background. You can also use borders or whatever you want to style, as long as you target the same index, you can give them the same colour.

This will result in a simple component:

simple annotated text

Now for a more advanced example where the amount of notes exceeds the amount of colours we have available, we have 6 references, but only 5 colours plus a fallback assigned:

When the priest killed a maiden in the metal church
Armored saints and warlocks watched the slaughter
Rage of the slayer forced the pretty maids
To kiss the Queen in crimson glory

  1. Reference to the band Judas Priest
  2. Reference to Iron Maiden.
  3. Reference to the band Metal Church 🤘⛪
  4. Reference to the band Armored Saints
  5. Reference to the band Warlock
  6. Reference to the band Slaughter

So every reference AFTER the first 5 will get a pale yellow colour, like the last one in this example. If I ever run into this case where I can not break up the source text enough, I could also number them with some old school footnote annotations like [1].

In case my styling changed since this blog post was published, here's a screenshot of what was supposed to be there 😛:

text annotation component

As you can see, I'm not amazing at picking examples because obviously the song references only bands, but you can totally pass longer texts without completely wrecking the layout.

Next steps for me would be to find a good way of highlighting the relevant annotation or maybe even displaying as a tooltip, which would improve the user experience.

What are you building that requires text annotation? Let me know!

Thank you for reading! If you have any comments, additions or questions, please tweet or toot them at me!