Annotations With React

Reetam Chatterjee
The Startup
Published in
7 min readJul 19, 2020

Annotations are those small notes or post-it(s) that you use in your daily life to give a little bit more information on a topic or item. It helps us understand things without going out of our own way to explore what it is or what it does. Its the same thing with web apps. When we create a web application with complex features, we often encapsulate their behavior so that it appears only when the user needs it, often showing it with the click of a button. A proper example is filters, where we often click on a filter button to show us all the ways the user can filter out the information to get what they need. And often users find it confusing and start hunting for features when they start using a new application. That is where annotations come in, they highlight each element in an app and show us what they do.

In this tutorial, let’s create an annotation for an element present in our app and show some information about it.

We will use Create React App to start off our project.

> create-react-app annotations-app-demo

Once CRA (read: Create React App) creates your app, go and delete the extra files and DOM elements created by CRA as a default template inside src folder.

We will use SCSS Modules to style our components so please go ahead and install node-sass.

> npm install node-sass

Once installed, lets start the react app which currently shows a blank page.

> npm start

Lets create a Button component. Go to src folder and create a new folder called components inside it. And inside the components folder, create a new folder called Button with two files inside it, Button.js and Button.module.scss . I will assume that you’re familiar with CSS Modules and go ahead. The Button component will render an HTML button. The catch here is, we will use React.forwardRef to pass a reference to the Button component from the parent, so that we can later use that ref to create our annotation. So, this is what our Button component file will look like:

As for the styles, here’s our Button.module.scss:

@mixin basicButtonCss {  
padding: 5px 10px;
border-radius: 10px;
font-family: 'Maven Pro', 'Arial';
box-shadow: 2px 5px 12px #888;
transition: all 0.5s ease;
cursor: pointer;
}
.btn-blue {
@include basicButtonCss;
background-color: #008cff;
color: #fff;
border: 1px solid #008cff;
font-size: 18px;
&:hover {
background-color: #1a6099;
transform: translateY(-5px);
box-shadow: 2px 10px 12px #888;
}
}
.btn-white {
@include basicButtonCss;
background-color: #fff;
color: #fff;
border: 1px solid #fff;
font-size: 18px;
&:hover {
background-color: #f0f0f0;
transform: translateY(-5px);
box-shadow: 2px 10px 12px #888;
}
}
.btn-flat {
@include basicButtonCss;
background-color: #fff;
color: #000;
border: 0;
font-size: 16px;
font-weight: 700;
box-shadow: none;
&:hover {
background-color: #f0f0f0;
box-shadow: 2px 2px 12px #888;
}
}
.btn-blue:disabled,
.btn-white:disabled,
.btn-flat:disabled {
cursor: not-allowed;
pointer-events: none;
}

Now, let’s rewrite our App.js file to add a title to our App and add two Buttons to it.

import React from 'react'; 
import styles from './App.module.scss'
import Button from './components/Button/Button';
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
showAnnotation: false,
}
this.emojiButtonRef = React.createRef()
}
toggleAnnotation = (state = false) => {
this.setState({
showAnnotation: state,
})
}
render() {
const { showAnnotation } = this.state
return (
<div className={styles.container}>
<h1 className={styles.introSection}>
Welcome to the Annotations App Demo
</h1>
<div className={styles.firstSection}>
<Button
onClick={() => false}
variant="white"
ref={this.emojiButtonRef}
>
<span role="img" aria-label='emoji'>
&#128512;&#128516;&#128151;&#128525;
</span>
</Button>
</div>
<div className={styles.secondSection}>
<Button
variant='blue'
onClick={this.toggleAnnotation.bind(this, true)}
ref={null}
>
Click Here to Toggle Annotation
</Button>
</div>
</div>
);
}
}
export default App;

The first Button contains some emoji and in this demo, we will add an annotation to this button to provide the user some details about it. The second Button acts as our trigger. When we click on it, it will show us the annotation. In actual applications, annotations are triggered in various ways, either directly on page load or when the element comes to the view-port. So, it is up to you on how you want to trigger it. We have added a ref to the emoji Button using React.createRef() so that we can later pass that ref to the Annotation component. Finally, we have created a method toggleAnnotation that toggles the visibility of the annotation. We have attached this method to the click event of the second Button.

Also, let’s create an App.module.scss and fill it up with:

@mixin mx-auto {  
margin-left: auto;
margin-right: auto;
}
.container {
@include mx-auto;
width: 100%;
max-width: 1440px;
}
.introSection {
padding: 40px 0;
width: 100%;
font-family: 'Maven Pro', 'Arial';
font-size: 48px;
text-align: center;
margin: 0;
}
.firstSection {
padding: 50px 0;
text-align: center;
width: 100%;
}
.secondSection {
position: fixed;
bottom: 0;
left: 0;
right: 0;
margin-bottom: 40px;
text-align: center;
width: 100%;
}

Now, lets create our Annotation Component.

Also, lets create an Annotation.module.scss file and add the following:

.backdrop {  
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #000;
opacity: 0.75;
z-index: 1;
}
.container {
position: fixed;
z-index: 2;
}
.annotationWrapper {
position: relative;
}
.annotationInfo {
position: absolute;
top: 100%;
}

Let’s have quick walkthrough on what it does. This component takes two main props (apart from children and className), one being the DOM Ref (elementRef )of the element it wants to annotate and the other is a method toggleClose , which closes the annotation once the user is done with it. Once the component is mounted, inside the lifecycle hook componentDidMount, we will create a copy of our element using a method called cloneNode. This method returns a clone of the Node (Element) that it was called upon and returns a shallow copy of that Node. The bonus here is, it doesn’t retain the event listeners of the original element, so we can feel free to attach our own. Also, it takes a boolean argument which specifies whether we need a deep clone of the element or not. In our case, we do! Therefore, we will create a deep clone of our annotation element using the elementRef prop we passed to our component and attach an event listener to the cloned element. The event listener in this case will be none other than our toggleClose prop. So, if the user clicks on the annotated element, we will close our annotation. Now, next what we will do is get the position of the original element with respect to the view-port. So, we will use getBoundingClientRect to get the top and left coordinates of the element with respect to view-port. Let’s take a brief look at our render method. It has two parent div inside a fragment. The first div represents our backdrop, which puts an overlay on the page. The other div is our main wrapper which contains our annotation. Both these div receive position: fixed; as a style so that we can position our annotation with respect to our view-port. So we have added a React ref to our annotation wrapper. Now, after getting our top and left coordinates, we will add these values as styles to our wrapper (using the React ref) so that we can position our annotation element exactly on top of our original element. Now finally, we need to add the cloned element to the DOM. To do that, we used another React ref to a div inside our annotation wrapper which will hold both the cloned element as well as the annotation text. Using this ref, we will append our cloned element to the DOM.

Now, our Annotation component holds the cloned element that we need to highlight. What about the annotation itself? Well, we can pass that as a children to our Annotation component. Let’s get to that, shall we?

We will now rewrite our App.js file:

Also, let’s add these styles to our existing App.module.scss file:

.annotation {  
padding: 5px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 10px;
text-align: center;
}
.annotationArrow {
@include mx-auto;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid #fff;
}
.annotationText {
font-family: 'Maven Pro', 'Arial';
font-size: 16px;
color: #000;
text-align: center;
}

Our App.js file now imports the Annotation component and we have passed the emoji button ref as a prop to it, along with the method that closes the annotation. As for our actual annotation, we have added it as a child to our Annotation component. Now, the final touch is to render the annotation only when we toggle it. Therefore we will render the Annotation component to the showAnnotation flag we have in our component state, when it changes to true. And we will remove it when the flag changes to false.

Now, to the demonstration. Go back to the browser and check. If you click on the toggle button, you will see that your annotation will appear. Clicking on the OK or the emoji button will close the annotation. Congrats!

Now, there’s a small exercise I’d like you to try out, as a Bonus exercise to make our Annotations more robust:

1. Use this app and trigger the annotation when your element comes into view using scroll events.

2. Store annotation trigger state in browser (hint: LocalStorage) so that user can see the annotation only once, during his/her first visit.

3. Find a way to disable the document scroll when the annotation is toggled.

Conclusion

We hereby end this tutorial on how to create annotations with React. If you want to check out the whole code, please feel free to do so. Below is the link.

I hope this article was helpful, and I hope to create more stuff like this.

--

--

Reetam Chatterjee
The Startup

Lazy React.js Developer | Loves Javascript more than his imaginary girlfriend