Skip to content

Slider thumb has a hardcoded aria-label that cannot be overridden #6782

Description

@victorzellweger-osu

Environment

- Operating System:  Darwin 25.6.0
- Node Version:      v22.15.1
- Nuxt Version:      - (Vue 3.5.35 + Vite 6, @nuxt/ui in Vue mode)
- Package Manager:   pnpm@10.29.3
- @nuxt/ui:          4.8.1 (also verified against v4 @ 6bd1dfc and v4.10.0)
- reka-ui:           2.9.8

Is this bug related to Nuxt or Vue?

Vue

Package

v4.x

Version

v4.10.0

Reproduction

The documentation examples reproduce this directly — no sandbox needed. Turn on a screen reader (VoiceOver Cmd+F5, or NVDA/JAWS on Windows) and Tab to the sliders on these pages:

Expected: a name supplied by the consumer on the first, and reka-ui's Minimum / Maximum and Value 1 of 3 on the second. Inspecting the role="slider" element in devtools shows the same thing without a screen reader.

The relevant source is two lines: https://github.com/nuxt/ui/blob/v4/src/runtime/components/Slider.vue#L136-L138

Description

Slider sets a static aria-label on the thumb, which is the element reka-ui renders with role="slider":

<SliderThumb ... :aria-label="thumbs === 1 ? 'Thumb' : `Thumb ${thumb} of ${thumbs}`" />

Slider.vue#L136-L138 — both the tooltip and non-tooltip branches

This produces three distinct problems.

1. A single-thumb slider can never be given an accessible name.

Its name is always Thumb, and every route to changing it is closed:

  • UFormField does not help. useFormField's ariaAttrs returns only aria-invalid and aria-describedby — no aria-labelledby. The field's label[for] targets the id on SliderRoot, a plain <span> that is neither a labelable element nor the element with role="slider", so it contributes nothing to the accessible name.
  • Attribute fallthrough does not help. <USlider aria-label="Volume" /> lands on SliderRoot, which has no role and is not focusable, so it is never announced.
  • The ui prop does not help — it accepts tv classes only.

Note that reka-ui deliberately leaves room for the consumer here: SliderThumbImpl resolves the label as $attrs['aria-label'] || getLabel(...), and getLabel() returns undefined for a single thumb. The wrapper's static string takes precedence over $attrs and closes that door.

2. For range sliders, the hardcoded label is less accessible than the primitive's default.

reka-ui's getLabel() returns ['Minimum', 'Maximum'] for two thumbs and Value {i} of {n} for three or more. Slider overrides all of them with Thumb {i} of {n}. On the most common range-slider use case — a price or date filter — this replaces two meaningful names with two meaningless ones. I don't believe that was the intent of #5313, which was aimed at the nameless single-thumb case, but it is the current behaviour.

3. aria-valuetext cannot be set.

Neither Slider nor reka-ui's slider sets aria-valuetext, and because $attrs cannot reach the thumb it cannot be supplied from outside either. Any slider whose raw number is ambiguous — durations, frequencies, unitless 0–1 ratios — announces a bare 5 where 5 milliseconds is what carries the meaning. The APG slider pattern calls for aria-valuetext in exactly this situation.

Impact

The control remains operable — keyboard interaction and value announcement are correct — so this is an identification failure rather than an operation failure. It is mild when a single slider sits alone on a page and the user is in browse mode, where the visible label can be inferred from reading order. It degrades sharply when:

  • Tabbing through a form, which skips static text, so the visible label is never spoken.
  • Several sliders share a screen, at which point every one of them announces identically and nothing distinguishes them.
  • Focus returns after a modal or re-render, leaving no way to tell which control is focused.
  • Voice control is used (Dragon, macOS/iOS Voice Control), which targets controls by accessible name. "Click Volume" fails; the only working phrase is "click Thumb", which is unguessable and ambiguous across multiple sliders.

Where a visible text label is present, this is a failure of WCAG 2.5.3 Label in Name (Level A) — the accessible name does not contain the visible label text. 1.3.1 and 4.1.2 are also implicated.

One consequence worth flagging: because Thumb is a syntactically valid name, automated tooling reports these sliders as passing. axe's aria-input-field-name rule catches a nameless slider but cannot detect a meaningless one, so the current behaviour makes the problem invisible to the checks most teams rely on.

Scope

This is distinct from #6749 / #6768, which move ariaAttrs from the root onto the thumb. That is a correct fix for a real adjacent problem, but since ariaAttrs carries no aria-labelledby, the accessible name is still Thumb after it merges. The three problems above are unaffected by it.

Additional context

Proposed direction (brief — happy to follow whatever shape you prefer):

Forwarding $attrs to the thumb and only applying the built-in label as a fallback would address all three problems at once, restore reka-ui's Minimum/Maximum defaults, and add no new API surface. An explicit thumbLabel prop, or adding aria-labelledby to useFormField's ariaAttrs, would each solve problem 1 alone. I'm glad to open a PR for whichever direction you'd prefer.

Prior art: #5313 introduced this and flagged it as provisional at the time —

This is not the best solution and I'm open to suggestions on how to best handle this. The aria-label should be customizable on a case by case basis so we should also add a prop to handle this or bind the related attributes from $attrs.

The PinInput precedent cited in that thread holds for PinInput, whose hardcoded name describes the control's actual purpose. A slider's purpose is caller-defined by nature, so a fixed name can't describe it.

Context for the report: this is what pushed us off USlider in an accessibility-first educational app — four sliders (threshold, strength, speed, frequency) that would all have announced as "Thumb" with units dropped. We fell back to a native <input type="range">, which gives correct naming and aria-valuetext for free but loses the design system styling. We'd much rather be using the component.

Logs

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions