47.4 ARIA Attribute Types in React and the DOM
Right, let’s talk about ARIA. You’ve probably heard the mantra: “No ARIA is better than bad ARIA.” It’s good advice, but it’s led to a weird fear of using it at all. The truth is, when you’re building a complex, dynamic web app, you need ARIA to bridge the gap between what your div-soup looks like and what it actually is for a screen reader user. TypeScript, being the wonderfully pedantic friend it is, can actually help you write good ARIA instead of avoiding it.
The first thing you’ll run into is the sheer verbosity of it all. You’ll go to add aria-labelledby to a div and get a red squiggly. This is TypeScript’s first lesson: ARIA attributes are, for deeply historical reasons, only fully defined on the correct HTML elements. You can’t put aria-required on a <div> because a generic div can’t be “required” in the same way an <input> can. This is the DOM’s built-in HTML attribute interfaces doing their job, and it’s actually a fantastic first line of defense against misusing ARIA.
So what do you do when you’re building a fancy custom dropdown out of divs and spans? You break the rules, responsibly. And TypeScript, bless its heart, will fight you. You have two main avenues of escape.
The aria-* Props in React
React’s type definitions for HTMLAttributes are just as strict as the DOM’s. Trying to slap aria-expanded on a <div> will make the TypeScript compiler sigh disappointedly. The official, sanctioned way to get around this is by using the aria-* props from the react package itself. They’re defined in a more flexible way for exactly this purpose.
import { useState } from 'react';
function CustomDropdown() {
const [isOpen, setIsOpen] = useState(false);
// This will cause a TypeScript error
// return <div aria-expanded={isOpen} onClick={() => setIsOpen(!isOpen)} />;
// This is the correct, React-approved way
return (
<div
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
onClick={() => setIsOpen(!isOpen)}
>
Select an option
</div>
);
}
See the key here? The moment you add the correct role="combobox", TypeScript suddenly understands. It’s like you’ve given your div a new ID badge. The type definitions say, “Ah, this is a combobox now! Of course it can have aria-expanded and aria-haspopup.” The role attribute is your on-ramp to valid ARIA.
When You Absolutely Must Force It
Sometimes, you’re integrating with a third-party library or doing something so weird that even role isn’t enough. This is where you dip into your bag of tricks. You can use a type assertion to tell TypeScript, “I know what I’m doing, now please be quiet.”
// Use this sparingly. It's the equivalent of yelling "I know!" at your compiler.
const dubiousProps = {
'aria-magic-future-attribute': 'true', // Something not yet in the types
} as React.AriaAttributes;
return <div {...dubiousProps} />;
A slightly better way is to use a library like @react-aria/utils, which provides a mergeProps function that often handles the type gymnastics for you. But the point is, the friction is there for a reason. That error is a prompt to ask yourself: “Am I using the right HTML element? Should I add a role? Is there a better way to structure this?”
The Gold Mine: aria-keyshortcuts and Literal Types
This is where TypeScript’s type system shines. Many ARIA attributes accept a specific set of literal string values. Guess what TypeScript is fantastic at? Enforcing literal string values.
function StatusMessage({ type }: { type: 'success' | 'error' | 'warning' }) {
// Let's map our prop to the correct ARIA role and value
const ariaRole = 'status';
const ariaLive = type === 'error' ? 'assertive' : 'polite';
return (
<div role={ariaRole} aria-live={ariaLive}>
{/** ... */}
</div>
);
}
Try changing ariaLive to "rude". Go on, try it. TypeScript will immediately stop you because "rude" is not assignable to "off" | "polite" | "assertive". This is incredibly powerful. It turns your compiler into an accessibility linter, catching typos and invalid values at compile time instead of waiting for a runtime test or, worse, a bug report from an assistive tech user.
The best practice? Lean into it. Use the strict types. Let the errors guide you toward correct usage. The designers of the ARIA spec made plenty of questionable choices (the sheer number of attributes is dizzying), but the TypeScript type definitions are your brilliant, pedantic ally in navigating them. They force you to think about the semantics of what you’re building, which is the entire point of accessibility in the first place.