Inertia Link override: no aria-current, invalid ariacurrentvalue attribute, and prefix-based active matching
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 6.9k
- Forks
- 1.1k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 57
Description
Environment
@nuxt/ui4.10.0- Vue + Vite (no Nuxt), Inertia.js v2, plugin configured as
ui({ router: 'inertia' }) - File under discussion:
dist/runtime/vue/overrides/inertia/Link.vue - Comparison point:
dist/runtime/vue/overrides/vue-router/Link.vue
Description
The router: 'inertia' Link override diverges from the vue-router one in three ways. Because every Nuxt UI component that renders a link (UButton, UBreadcrumb, UNavigationMenu, UDropdownMenu, …) goes through this component, all three affect the whole library in Inertia mode.
1. aria-current is never emitted
isLinkActive (inertia/Link.vue:109-123) only feeds linkClass (:124-130) and the active slot prop. The rendered anchor never receives aria-current.
The vue-router variant does emit it (vue-router/Link.vue:119 and :136):
...exact && isExactActive ? { 'aria-current': props.ariaCurrentValue } : {},
The Inertia variant has no equivalent, so an Inertia app that migrates its navigation onto Nuxt UI silently loses the current-page announcement for screen-reader users.
2. ariaCurrentValue leaks onto the DOM as an invalid attribute
ariaCurrentValue is declared with a default (inertia/Link.vue:26):
ariaCurrentValue: { type: String, required: false, default: "page" },
but it is not in the reactiveOmit list at :71:
const routerLinkProps = useForwardProps(reactiveOmit(props, "as", "type", "disabled", "active", "exact", "activeClass", "inactiveClass", "to", "href", "raw", "custom", "class", "target", "rel", "noRel"));
so useForwardProps passes it to ULinkBase (:150-166), which does not declare it either, so it lands in $attrs and reaches the DOM as ariacurrentvalue="page".
vue-router/Link.vue omits it from its reactiveOmit list too, but there it is harmless: RouterLink declares ariaCurrentValue as a real prop and consumes it. inertia/LinkBase.vue (and @inertiajs/vue3's Link) declare no such prop.
Because the prop carries a default, the attribute appears even on components that render no link at all. Measured on a page with three Nuxt UI components — one <UButton to> and two <UButton> with no to — all three rendered ariacurrentvalue="page", client-side and in SSR output.
Minimal repro:
<UButton>no link at all</UButton>
<!-- renders: <button ... ariacurrentvalue="page"> -->
3. Active matching uses a raw prefix against a query-carrying URL
inertia/Link.vue:119:
if (!props.exact && page.url.startsWith(href.value)) {
return true;
}
usePage().url in Inertia includes the query string, and startsWith is a raw prefix test. Three consequences:
- A query string defeats the match.
page.url = '/inventory?search=x'withto="/inventory"is active (prefix holds), butto="/inventory"onpage.url = '/inventory?search=x'where the link itself carries a query — or any link whose href is longer than the bare path — is not. More importantly the reverse case,to="/inventory/adjust"while on/inventory?tab=1, behaves unpredictably. to="/"is active on every page, since every URL starts with/.- Segment boundaries are ignored:
to="/inventory"reports active on/inventory-adjustments.
The vue-router variant delegates this to RouterLink's isActive/isExactActive, which are path- and segment-aware, so the Inertia variant is the odd one out.
Suggested fix
- Add
...isLinkActive ? { 'aria-current': props.ariaCurrentValue } : {}to the twov-bindobjects ininertia/Link.vue's template (absent, not"false", when inactive). - Add
"ariaCurrentValue"to thereactiveOmitlist at:71. - Compare on the path only, with segment boundaries:
const pathOf = (url) => {
const path = url.split('#')[0].split('?')[0]
return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path || '/'
}
// non-exact
const current = pathOf(page.url)
const target = pathOf(href.value)
return target === '/' ? current === '/' : current === target || current.startsWith(`${target}/`)
Happy to open a PR for this if the approach looks right.
Additional context
We are carrying this as a local build-time patch on the vendored module while migrating a production Inertia app onto Nuxt UI, and would rather drop it than maintain it.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with dist/runtime/vue/overrides/inertia/Link.vue and compare it with dist/runtime/vue/overrides/vue-router/Link.vue, focusing on the active-state logic, rendered bindings, and reactiveOmit list. Verify the Inertia override against the reported link and no-link examples; done means aria-current is present only when appropriate, ariaCurrentValue does not reach the DOM, and path matching handles queries, root, and segment boundaries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nuxt, typescript
- Domain
- accessibility, frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100