Building a commentary sidebar in React

This is the first of two parts. Part two is here.
The sidebar is responsive and collapses inline on smaller screens

Over the past weeks, I’ve been building this blog and writing articles for it. I’ve spent a lot of time thinking about the reader. Who are they? Why are they here? It’s often hard to know how much detail to go into and how much background knowledge to assume.

On the one hand, people are busy. Attentions are short and time is precious. I’d do well to keep articles lean, focussed and to the point. But that misses the point. People are here for enjoyment and just presenting the cold hard facts isn’t going to cut it.

I’ve settled on an approach I hope will cater to a variety of readers. I keep the main body of content concise and on topic but enrich it with commentary on the side. This leaves room for elaboration, tangential remarks and all the whimsy I can mustard.

However, this commentary hides complexity. When viewed on a large-enough screen, it’s presented in a sidebar next to the main content. Ideally, comments sit as close as possible to the word or phrase they refer to. This helps flow and improves continuity.

In this article, I’ll explain the mechanics of how this works in my React app. As you’ll see, there’s a lot to consider to make a robust, responsive and semantic solution.

Structuring the DOM

Putting React to one side for a moment, let’s first consider how to structure the DOM. HTML5 has the perfect element for our needs:

The HTML <aside> element represents a portion of a document whose content is only indirectly related to the document’s main content. Asides are frequently presented as sidebars or call-out boxes.  MDN web docs

That leaves the question of whether to group all commentary together into a single <aside> or to have one per comment. I decided on the latter as I think this is more meaningful. Comments are independent of each other so the DOM should reflect that.

Here’s how that looks in code:

<main>  <article>    <p>The article's first paragraph.</p>    <aside>A comment on the above.</aside>    <p>The article's second paragraph.</p>  </article></main>

Comments interspersed with content

This should be friendlier to search engines, too. Related content is closer together.

Laying things out

As you can see above, there’s no DOM element for the sidebar. I think of a sidebar as a way to present <aside> elements. It really ought to be a presentational concern.

There are a number of CSS techniques we could use to create a sidebar. There’s flexbox, floats, relative and absolute positioning. This seemed simplest to me:

main {  max-width: 50%;         // Article width  padding-right: 50%;     // Sidebar width  background: #eee;       // Sidebar color  position: relative;  article {    padding: 1rem;    background: white;  }  aside {    position: absolute;    right: 1rem;    width: calc(50% - 2rem);  }}

One way of creating a sidebar with CSS

This works by splitting the container in two. The sidebar comments live in the space created by the padding-right rule. They use absolute positioning which removes them from the document’s flow and right: 1rem to place them in the padded region.

We can left-position the <aside> elements within the sidebar by setting their width. This is calculated as 50% - 2rem to create the illusion of padding - by leaving an equal amount of space on the left and right. Here’s how it looks:

Using padding to create a sidebar on the right

This isn’t quite right because the comment is placed next to the second paragraph instead of the first. We could fix it by moving the <aside> above the first paragraph but then the DOM would be out of order and wouldn’t read correctly on mobile.

Further complications

To make matters worse, we also want to be able to position commentary next to specific words or phrases. For example:

A badly-placed comment thatdoesn’t line up

We can use absolute positioning for this but it’s not so straightforward. The paragraph is free-flowing text which means it’s hard to predict where the words will be. It depends on the width of the browser, which font is loaded and its line height.

We could change the DOM and move the <aside> into the text element but again, that would mean the content’s out of order. We’d end up with a horrible jumble:

<p>In this paragraph, I want to comment on<span>  <em>these words</em>  <aside>Does red clash with the theme?</aside></span>which are in the middle of the paragraph.</p>

It’s also not good practice to do this. React gives us a warning:

validateDOMNesting(...): <aside> cannot appear as a descendant of <p>.

The fundamental problem is there’s no way in CSS to position elements relative to arbitrary things. It would be neat if we could do something like this, but we can’t:

aside {  position: element(#thing);  top: 0;}

I spent a while thinking of other ways to do this in CSS. Flexbox’s order property is interesting because it allows for a difference in how the DOM is structured to how elements are presented. Ultimately, I couldn’t find a way and resorted to JavaScript.

React

In React, I created an <Aside> component that positions itself relative to a target element. It encapsulates the behavior of figuring our what its position should be and listens to events that might invalidate that, such as resizing the window.

Here’s a first version:

const Aside = ({ target, children }) => {  const [style, setStyle] = useState(null);  const align = () => {    const rectangle = target.current.getBoundingClientRect();    const offset = window.scrollY + rectangle.top;    setStyle({ top: offset });  };  useEffect(() => {    align(); // Align when the component is mounted.    const listener = window.addEventListener("resize", align);    return () => window.removeEventListener("resize", listener);  }, []);  return (    <aside className={css.aside} style={style}>      {children}    </aside>  );};export default Aside;

A React component that aligns itself with a target

There are really two main parts to it:

  1. The align function calculates the y-coordinate of the target which is a reference to a DOM element. It sets the top style property to this.
  2. The useEffect hook aligns the component when it’s first mounted and binds a listener to re-align the component if the browser is resized.

Here’s how it looks for a real example. Notice the comments move around in the sidebar, repositioning themselves next to their target content.

Re-aligning comments while resizing the browser

To use the <Aside> component, I create a ref to the target content then pass that in:

<p>In this paragraph, I want to comment on<em ref={r=createRef()}>  these words</em>which are in the middle of the paragraph.</p><Aside target={r}>  Does red clash with the theme?</Aside>

How to use the <Aside> component

Breaking the fourth wall

As I write this article, I’m using this component a lot! Let’s peek at the code:

### Breaking the fourth wallAs I write this article, I’m using this component a lot!Let’s peek at <span ref={r=createRef()}>the code</span>:<Aside target={r}>Well this is unexpected. {/* Waves to self. 👋 */}</Aside>

Using the <Aside> component in this article

If your browser is wide enough, you should see ‘Well this is unexpected’ on the right-hand side of ‘the code’. Otherwise, it’ll appear in the main flow.

In some cases, there’s no word or phrase in the DOM to refer to. This is the case with images, videos and code blocks that use the <pre> tag. For these cases, I built an escape hatch that lets me move comments down by a fixed amount:

<span ref={r = createRef()} /><Aside target={r} moveDown={70}>This aside is moved down 70 pixels.</Aside>

This amount is then added to the offset:

const Aside = ({ target, children, moveDown = 0 }) => {  // ...  const align = () => {    // ...    const offset = window.scrollY + rectangle.top + moveDown;    setStyle({ top: offset });  };  // ...};

Moving comments down by a fixed amount

All told, this serves very well as a means of commenting on variety of things: text, images, code and (as we’ve seen) even comments themselves!

Incremental improvements

In part two of this article we’ll be refine our component in a number of ways:

  • We’ll cope with slow-loading images and fonts
  • We’ll handle a race condition with React refs
  • We’ll add debouncing, for a smoother browser experience
  • We’ll extract code into hooks to make things tidy
  • We’ll stare into the face of evil, aka. print layouts

Sounds exciting!? I think so... See you in part two.