aria-label-split-text-rendering
Use when one text value is rendered across two or more elements for styling — a typed prefix in one ink and the completion in another, a highlighted search match, a two-tone label — and a getByRole qu
SKILL.mdplugins/react-frontend-sdlc/skills/aria-label-split-text-rendering/SKILL.md
GitHubaria-label for split text rendering
Profile keys consumed
architecture.source_rootframework.ui
Overview
The accessible name of an element with no explicit label is computed from its contents, and the
algorithm joins each child element's text with a space. Splitting one word or phrase across two
elements for styling therefore produces a name that does not match the visible text. Give the
container an explicit aria-label carrying the whole, unsplit value.
When to use
- An autocomplete, search, or select option renders the typed prefix and the completion as separate elements.
- A test cannot find a control by its visible name, or finds it under a name with a stray space.
- Any component splits a single value for colour, weight, or emphasis.
- Not for: text that is genuinely two separate pieces of content (a title plus a description); those want real structure, not a flattened label.
Applicability by repository shape
- React SPA shape (feature modules under the source root, a bootable app, an aggregate CI target): partial — the rule holds for any component, but no listbox or autocomplete option renderer exists today; the nearest live cases are labelled form controls.
- Next.js app shape (routed pages, no aggregate duplication gate): partial — same; the shared
components barrel under
architecture.source_rootships no option renderer. - Component-library shape (Storybook-first, no bootable app, published package): yes — the
shared option renderer under the field-controls directory of
architecture.source_rootapplies it for the search and multi-select listboxes, and its unit tests query by the full option name.
Core pattern
// Broken: two runs, so the name-from-contents algorithm joins them with a space.
<li role="option">
<span style={headInk}>{head}</span>
<span style={tailInk}>{tail}</span>
</li>
// Fixed: the row carries the whole value, and `pre` keeps the real separator.
React.createElement(
'li',
{ ...optionProps, 'aria-label': label },
<Box component="span" sx={{ color: headInk, whiteSpace: 'pre' }}>{head}</Box>,
<Box component="span" sx={{ color: tailInk, whiteSpace: 'pre' }}>{tail}</Box>
);
Two details make the fix complete:
whiteSpace: 'pre'on each run. A listbox row is usuallydisplay: flex, so each run is a flex item that trims its own leading and trailing whitespace. A space that falls on the split boundary disappears and the two words render glued together.React.createElementrather than a JSX spread.react/jsx-props-no-spreadingis an error in the React SPA and component-library shapes with a short exception list that does not include a bareli, so the prop bag a component library hands the renderer is passed as an explicit object. That satisfies the rule at the source instead of suppressing it. TheBoxwrapper above is theframework.uiprimitive; use whatever styled primitive the configured UI library provides.
Verification
Assert the name, not the markup: getByRole('option', { name: '<full value>' }) must resolve, and
the rendered text content must equal the same string. Both must pass — a matching aria-label with
a broken visible split is still a mismatch between what is seen and what is announced.
In review, the shape to spot is any element carrying a role whose name comes from contents and which holds more than one text-bearing child; check that its label names the whole value.
Common mistakes
- Adding
aria-labelto one of the runs instead of the container — the container's name is still computed from contents. - Writing an
aria-labelthat paraphrases the value; WCAG 2.5.3 Label in Name requires the accessible name to contain the visible text. - Fixing the space with a
or a literal space element instead ofwhiteSpace: 'pre', which changes the visible text and re-breaks the name. - Leaving the label static when the runs come from a dynamic value — derive both from the same bound value so they cannot drift.