
Explore focusable, clickable, tabbable or active states
Explore focusable, clickable, and active states for accessibility, and determine the focusable state using JavaScript.
Determining whether an element is interactive or not can be crucial for various purposes, such as improving accessibility, enhancing user experience, testing purpose or debugging issues.
An interactive element is one that can receive focus, be clicked, be active, or be tabbable, allowing users to engage with it in some way. Let’s find out how to determine if an element is focusable or active using JavaScript.
What is the difference between an interactive element that can receive focus, be clicked, be active, or be tabbable?
The terms focusable
, clickable
, active
, and tabbable
refer to different aspects of an element’s interactivity. Here’s a breakdown of each:
- Focusable: an element is focusable if it can receive keyboard focus, meaning it can be selected and highlighted using the keyboard (usually by pressing the Tab key). Focusable elements typically have a visible outline, border or shadow when focused.
- Clickable: an element is clickable if it can be activated by a mouse click or a touch event. This usually triggers an event handler or executes a specific action.
- Active: an element is active when it is being interacted with, such as when a user is tabbing, clicking, or dragging on it. The active state is usually indicated by a visual change, like a changed background color, border style or shadow.
- Tabbable: an element is tabbable if it can be reached by pressing the Tab key, allowing users to navigate to it using their keyboard. Tabbable elements are usually focusable, but not all focusable elements are necessarily tabbable. For example, element with a negative
tabindex="-1"
is removed from the default tabbing order, while still allowing it to be focused programmatically using JavaScript.
Determine if an element is clickable
To determine if an element is clickable, you can check several properties. However, keep in mind that this is not an exhaustive list, and there might be other factors that affect an element’s clickability. However, this should cover most common cases.
function isElementClickable(element) {
if (element === null || typeof element === 'undefined' || element.nodeType !== Node.ELEMENT_NODE) {
return false;
}
const styles = window.getComputedStyle(element);
if (
element.disabled ||
element.ariaDisabled === 'true' ||
element.tabIndex < 0 ||
styles.pointerEvents === 'none' ||
styles.display === 'none' ||
['hidden', 'collapse'].includes(styles.visibility) ||
styles.opacity === '0' ||
styles.cursor === 'not-allowed' ||
styles.cursor === 'none'
) {
return false;
}
// Additional check for overlapping elements (simplified)
// This is a basic check; more sophisticated checks might be needed
const rect = element.getBoundingClientRect();
const elementsFromPoint = document.elementsFromPoint(
rect.left + rect.width / 2,
rect.top + rect.height / 2,
);
if (elementsFromPoint.includes(element) === false) {
return false; // Another element is on top
}
return true;
}
Determine if an element is focusable
Determining if an element is focusable can be done on two levels:
- Level 1: Determining if an element is a type of focusable element.
- Level 2: Determining if the element can receive focus right now.
Determining if an element is a type of focusable element
const FOCUSABLE_ELEMENTS_CSS_SELECTOR = [
'[tabindex]',
'[contenteditable=""]',
'[contenteditable="true"]',
'a[href]',
'area[href]',
'audio',
'button',
'embed',
'form',
'iframe',
'input:not([type="hidden"])',
'label',
'link',
'object',
'select',
'summary',
'textarea',
'video'
];
function isFocusableType(element) {
const focusableElementsSelector = FOCUSABLE_ELEMENTS_CSS_SELECTOR.join(', ');
return element.matches(focusableElementsSelector);
}
Determining if the element can receive focus right now
function canReceiveFocus(element) {
let originalFocus = document.activeElement;
if (originalFocus !== null) {
while (originalFocus.shadowRoot?.activeElement) {
originalFocus = originalFocus.shadowRoot.activeElement;
}
}
let result = false;
if (typeof element.focus === 'function') {
try {
element.focus({
preventScroll: true
});
result = element === document.activeElement;
if (originalFocus) {
originalFocus.focus({
preventScroll: true
});
}
} catch (_error) {
// Handle scenario when method "focus" throws an exception
}
}
return result;
}
The function covers the following cases:
- It attempts to focus the element: by trying to call the focus method on the element, we’re checking if the element can actually receive focus. This approach is more reliable than just checking the element’s properties or attributes.
- It handles exceptions: if the focus method throws an exception, the function catches it and returns false, indicating that the element cannot receive focus.
- It restores the original focus: after attempting to focus the element, the function restores the original focus to the previous active element, which is a good practice to avoid disrupting the user’s interaction with the page.
- It checks for shadow DOM: the function also checks if the original focus is within a shadow DOM, which is an important consideration when working with web components.
Determine which element is active now
To determine which element is active now, you can use the document.activeElement
property. This property returns the currently active element in the document, which is the element that has focus.
const activeElement = document.activeElement;
console.log(activeElement);
Note that document.activeElement
can return null
if no element is currently active, such as when the document is first loaded or when the user has not interacted with the page yet.
Also, if you’re working with shadow DOM, you may need to recursively check the activeElement
property of the shadow root to find the active element within the shadow DOM.
Here’s an example of how to recursively check the activeElement
property of the shadow root:
function getActiveElement(element) {
if (element === null || document.hasFocus() === false) {
return null;
}
if (element.shadowRoot && element.shadowRoot.activeElement) {
return getActiveElement(element.shadowRoot.activeElement);
}
return element;
}
const activeElement = getActiveElement(document.activeElement);
console.log(activeElement);
This function recursively checks the activeElement
property of the shadow root until it finds the active element within the shadow DOM.
The document.hasFocus()
used above is a method that returns a boolean value indicating whether the document (or any element within it) currently has focus. In other words, it checks if the user is currently interacting with the document, either by clicking on an element, typing in a form field, or navigating through the document using the keyboard.
Comments