Trap focus in specific website area

Sometimes it may be useful to trap the focus inside a specific area of the website so that no other focusable elements outside of this area can be reached (for example via using the tab key). Here I show you how to do that.

The starting point

I searched for a solution for that problem while updating my Fanoe theme. The theme comes with an off-canvas sidebar, which is displayed above the other content since the update, instead of pushing the content away. You can see it in the following GIF:

If the sidebar is displayed, the focus should be trapped inside it, so keyboard user cannot navigate to focusable elements which are not part of the sidebar until they close the sidebar via the button or Esc key.

Implementing of the solution with ally.js

The JS library ally.js comes with a solution for the problem which is shown in a tutorial. The important markup inside my theme looks like that:

<button class="sidebar-button -open">
	<span class="screen-reader-text">Show Sidebar</span>
	<span aria-hidden="true">≡</span>
</button>
<aside class="sidebar" role="sidebar">
	<button class="sidebar-button -close">
		<span class="screen-reader-text">Close Sidebar</span>
		<span aria-hidden="true">≡</span>
	</button>
	<div class="sidebar-content">
		<!-- The widgets -->
	</div>
</aside>

And that is the important JS part (I use Webpack to run the import, and Babel to convert ES6 syntax to ES5 – I wrote a post about setting that up):

import ally from 'ally.js/ally';

/**
 * Custom JavaScript functions.
 *
 * @version 2.0.3
 *
 * @package Fanoe
 */
{
	/**
	 * Get the html element.
	 *
	 * @type {Element}
	 */
	const root = document.documentElement;

	/**
	 * Get the elements for sidebar handling.
	 */
	const openButton = document.querySelector('.sidebar-button.-open');
	const closeButton = document.querySelector('.sidebar-button.-close');
	const sidebarElem = document.querySelector('.sidebar');
	let disabledHandle;
	let tabHandle;
	let keyHandle;

	/**
	 * Call sidebar function sidebar button click.
	 */
	openButton.addEventListener('click', sidebar, false);

	/**
	 * Call sidebar function sidebar button click.
	 */
	closeButton.addEventListener('click', sidebar, false);

	/**
	 * Catch clicks on the document to close sidebar on mouse click
	 * outside the open sidebar.
	 */
	document.body.addEventListener('click', function (e) {
		/**
		 * Check if the sidebar is visible.
		 */
		if (root.classList.contains('active-sidebar')) {
			/**
			 * Check if the click was made on the .sidebar element, not the .sidebar-content.
			 */
			if (e.explicitOriginalTarget.classList.contains('sidebar')) {
				disabledHandle.disengage();
				tabHandle.disengage();
				root.classList.remove('active-sidebar');
			}
		}
	}, false);

	/**
	 * Function to display and hide sidebar.
	 *
	 * @link https://allyjs.io/tutorials/accessible-dialog.html
	 */
	function sidebar() {
		/**
		 * Toggle .active-sidebar class.
		 */
		root.classList.toggle('active-sidebar');

		/**
		 * Check for class name to know if the
		 * sidebar is currently visible or not.
		 */
		if (root.classList.contains('active-sidebar')) {
			disabledHandle = ally.maintain.disabled({
				filter: sidebarElem,
			});

			tabHandle = ally.maintain.tabFocus({
				context: sidebarElem,
			});

			keyHandle = ally.when.key({
				escape: closeSidebarByKey,
			});
		} else {
			disabledHandle.disengage();
			tabHandle.disengage();
		}
	}
}

The code is strongly oriented on the tutorial which I linked above. First, we import the ally lib, get the root element and save the buttons and the sidebar element. After that, we add the needed EventListeners:

  • Click on the buttons. This runs the sidebar() function, which displays or hides the sidebar.
  • Click on the body element. After that, we check if the sidebar is currently visible. If so, we test if the user clicked the .sidebar element. If that is the case, the click was done on the greyed-out area outside of the sidebar content (that is because the grey area was created with a .sidebar::before rule).

In sidebar(), we toggle the active-sidebar class on the root element. After that, we check if the class is set (so the sidebar was displayed by the button click). In that case, ally.maintain.disabled disables all elements outside the sidebar for being focused. With ally.maintain.tabFocus we set the tab focus on the sidebar element, so the user does not jump to the browser UI after the last focusable element in the sidebar but to the first focus element of the sidebar.

Finally, we define that pressing the Esc key runs the sidebar() function to close the sidebar. If the active-sidebar class is not set, we remove all the restrictions.

Related posts

Leave a Comment

Your email address will not be published. Required fields are marked *