When something happens in an application that has no direct visual outcome, it makes sense to tell the user with a message that it happened. One example where I implemented that lately is the Dear Console,… project. When you activate any of the copy icons, you get a message showing right where you were that the text is now copied into your pasteboard.
This is a pretty common task, but often you see implementations that forget some basic aspects of accessibility. Pretty common are notification bars on top of the screen or a toast message bottom right. The problem with those is that you assume that people can see the whole screen. But there is a large group of people that only view a small section of the screen, either because they use a mobile device, or because they need to zoom in to interact with your product.
This is why it is important to give the feedback message right where the interaction happened. There are several ways to achieve that, and here is one that I found generic enough and sturdy in its implementation. You can see it in action in this codepen.
Obvious mistake: Don't trust the mouse position
The first thing most people would do to position an element where the interaction happened is to read the mouse position and display it accordingly. This fails because of two reasons: first of all, not everybody uses a mouse and secondly, reading the mouse position in a world of positioned elements, scrolling interfaces and documents in iframes is complex.
Better: reading the position of the target element
One of the more "this is a mouthful" DOM methods is element.getBoundingClientRect, but it is incredibly useful. It gives you the location of an element in the document and its dimensions as an object.
You can use this to position another element right next to the current one by reading the top
and right
properties. If you use an element that is keyboard accessible, like a button or a link, this means you can support all kind of users. In the case of this demo, we use buttons.
Styling the notification
.popup {
position: fixed;
left: calc(var(--element-x) * 1px);
top: -20em;
transition: 400ms;
/* more styles */
}
.copied .popup {
top: calc(calc(var(--element-y) * 1px) - .5em);
left: calc(calc(var(--element-x) * 1px));
}
We position the notification message as fixed
on the screen and give it a left
and top
property. To make things smoother, we also add a transition. As the values of left and top we use CSS custom properties. These we will get later on from JavaScript and the getBoundingClientRect
. As they are without the 'px', we need to use calc()
to tell CSS what to do. We position the message off-screen (top
as -20em
), and move it to the element position when there is copied
class on a parent element. This allows us to keep all the styling in CSS. JavaScript is only needed to get the values and to add and remove the parent class.
Showing, hiding and positioning the message with JavaScript
The script to make the rest happen isn't too complex. You can see it in its entirety here with comments on what the code means.
// hide the message after 1.5 seconds
const timeout = 1500;
// create an element with the class popup and add it to the
// document
const body = document.body;
let copypopup = document.createElement('output');
copypopup.classList.add('popup');
copypopup.innerText = '☑️ Copied!';
copypopup.tabIndex = -1;
body.appendChild(copypopup);
/*
When someone clicked one of the buttons
add a class of `copied` to the document body.
Undo this after 1.5 seconds
*/
let copythis = elm => {
body.classList.add('copied');
setTimeout(() => {
body.classList.remove('copied');
}, timeout);
}
// If there is a click on the body element
body.addEventListener('click', e => {
// test that there is currently no active message
if(!body.classList.contains('copied')){
// get the element the click happened on and
// ensure it was a button
let t = e.target;
if (t.tagName === 'BUTTON') {
// get the position of the button and set
// two CSS custom properties accordingly
let x = t.getBoundingClientRect().right;
let y = t.getBoundingClientRect().bottom;
let root = document.documentElement;
root.style.setProperty('--element-x', x);
root.style.setProperty('--element-y', y);
// call the function to show the message
copythis(e.target);
}
}
});
Possible enhancements
There are still a few things we can do better. For one thing, we still are in a visual context here. Users of assistive technology like screenreaders who can't see what's going on, don't get any information that we successfully copied something to the clipboard. In the following recording the buttons are announced but the interaction with them doesn't say anything about the notification.
This fixed codepen example works around this problem by using an output
element in the source and changing its value.
This makes it work with screenreaders. Interacting with the buttons now announces the text of the notification and what was copied.
The last thing I am not sure about is to automatically hide the notification after an amount of time without giving a user to prevent that is the best way. Got an even sturdier solution? Please comment!