Sanely manage <kbd>Tab</kbd> accessibility in React with `<Untabbable>` and the `useTabIndex` hook!
npm install react-tabindexSanely manage Tab accessibility in React with and theuseTabIndex hook!
- Support for nested tabbable states – for nested accordions, menus, modals,
etc.
- Performs no manual DOM manipulation, querying, or ref management – just pure
declarative React.
- Less than 1 kB minified, no dependencies.
Install with your choice of package manager:
``console`
$ yarn add react-tabindex
$ npm install react-tabindex
The useTabIndex hook returns a value to pass to the tabIndex prop on
elements of your choosing. If wrapped in an active ancestor, thattabIndex value will automatically be set to -1, making the elements
untabbable.
Start using useTabIndex for any tabbable elements you render in your
components. Remember to get them all if you want correct behavior! Buttons,
links, inputs, and all the rest.
`js
import { useTabIndex } from 'react-tabindex';
function ExampleButton() {
const tabIndex = useTabIndex();
return ;
}
`
If you have a desired tabIndex value (for example, from props) you can pass
that as an argument:
`js
import { useTabIndex } from 'react-tabindex';
function ExampleButton({ children, tabIndex }) {
// Override the input tabIndex with the result of useTabIndex.
tabIndex = useTabIndex(tabIndex);
return ;
}
`
Now, when a section of your app becomes untabbable, wrap it in an active
and toggle the prop. A good example is carousel slides that are not
visible:
`js
import { Untabbable, useTabIndex } from 'react-tabindex';
function Carousel({ activeIndex, items }) {
return (
{items.map((item, index) => (
// Make all carousel items except the active one untabbable.
// NOTE: Instead of conditionally adding/removing the active
// wrapper, you should instead toggle its prop (otherwise,
// React will remount the entire subtree each time, since the
// structure is changing).
))}
);
}
function IntroSlide() {
const tabIndex = useTabIndex();
return (
Prestige Worldwide
);
}
`
For best results, make your component library (design system) primitives like
Sometimes you have nested regions that need to become untabbable. A good example
would be a nested accordion/collapsible style menu. It is fine (and expected) to
nest elements in this scenario, and it will behave correctly outUntabbable
of the box. That is to say, **any ancestor being active willactive
override the state of any Untabbable descendants**.
In the following example, a nested collapsible menu uses when its
children are collapsed. Even if a submenu is in the expanded state
(), a parent menu being collapsed will cause thatUntabbable
parent’s to override the state of any Untabbable descendants.
You may be wondering: in a real app, wouldn’t you use a property like hiddendisplay: none
or CSS like on collapsed elements, which naturally removes anyUntabbable
elements therein from the tab order? That may be the case – but you may also
want to animate the content that is being expanded or collapsed, and _during
that time it will still be visible and tabbable_ – so it’s a good idea to use anyway even if the final resting state of the collapsed content isdisplay: none
already removed from the tab order. Other times, like with a carousel, the
inactive items may never be set to in the first place, soUntabbable becomes essential.
`js
function Collapsible({ id, label, children }) {
const [expanded, setExpanded] = useState(false);
return (
function App() {
return (
);
}
`
The Untabbable component has a reset prop that allows you to ignore theactive
state of any ancestors, causing it to no longer inherit their state.Untabbable
This feature is primarily useful for modals, which would otherwise inherit the
app’s state, but instead intentionally “break out” of the
underlying content. Let’s take a look.
` Content in here is still tabbable.js
function App() {
const [isModalOpen, setModalOpen] = useState(false);
return (
{isModalOpen ? (
It worked!
) : null}
);
}
function Button({ tabIndex, ...rest }) {
tabIndex = useTabIndex(tabIndex);
return ;
}
function Modal({ onClose, children }) {
return ReactDOM.createPortal(
// Use reset so that modal content does not inherit the Untabbable state`
// from the rest of the app, which is hidden when the modal is open.
{children}
document.body
);
}
In this simple example, there is only one modal. But you can imagine
implementing a more advanced scenario with multiple stacked modals, which is
also possible and will use the same reset feature. See the test suite for such
an example.
For this to work correctly on all elements in your app (and not leave any
tabbable when they should be untabbable), you must ensure that all focusable
elements use the useTabIndex hook. This can be tedious and hard to remember,
which is why it’s a good idea to incorporate it into your component library
(design system) and make sure everyone uses it.
If you are rendering any static HTML (using dangerouslySetInnerHTML, foruseTabIndex
example with data from a CMS), unfortunately there will be no way to ensure that
content uses , since it is not controlled by React.
If you have other content on the page outside of your React root, or rendered by
other non-React JavaScript widgets, there will likewise be no easy way to apply
useTabIndex to that content. You will have to resort to manual DOM
manipulation side effects to handle such cases.
Uses tabbable under the hood to
automatically identify tabbable elements inside an Untabbable region andtabindex
modify their attribute using manual DOM manipulation. This has pros
and cons.
Pros
- It’s more automatic as the tabbable elements don’t have to individually use a
special hook to update their tabIndex prop.Untabbable
- It will also work with static HTML inside the region that is notdangerouslySetInnerHTML
controlled by React (e.g. ).
Cons
- It is usually not kosher for a React component to have its DOM modified from
another React component.
- Can lead to inconsistent state. For example, let’s say an Untabbable regiontabindex
becomes active, and all its descendants have their attributetabindex
modified. Since those descendants may be dynamic React components, it’s
possible for them to still be adding, removing, or modifying content after the
DOM was already modified. For instance, let’s say one is loading data, then
renders new elements when the data is available – that updated content will
not have the correct , since the Untabbable already ran its DOMtabindex
manipulation. It’s also possible that React will unknowingly undo the
modification performed by the Untabbable ancestor duringreact-tabindex` is that state is
rerendering. The advantage of using
guaranteed to stay in sync and reactive (which is the whole idea behind
React)!