Environment
@nuxt/ui 4.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' with to="/inventory" is active (prefix holds), but to="/inventory" on page.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 two v-bind objects in inertia/Link.vue's template (absent, not "false", when inactive).
- Add
"ariaCurrentValue" to the reactiveOmit list 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.
Environment
@nuxt/ui4.10.0ui({ router: 'inertia' })dist/runtime/vue/overrides/inertia/Link.vuedist/runtime/vue/overrides/vue-router/Link.vueDescription
The
router: 'inertia'Linkoverride diverges from thevue-routerone 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-currentis never emittedisLinkActive(inertia/Link.vue:109-123) only feedslinkClass(:124-130) and theactiveslot prop. The rendered anchor never receivesaria-current.The
vue-routervariant does emit it (vue-router/Link.vue:119and:136):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.
ariaCurrentValueleaks onto the DOM as an invalid attributeariaCurrentValueis declared with a default (inertia/Link.vue:26):but it is not in the
reactiveOmitlist at:71:so
useForwardPropspasses it toULinkBase(:150-166), which does not declare it either, so it lands in$attrsand reaches the DOM asariacurrentvalue="page".vue-router/Link.vueomits it from itsreactiveOmitlist too, but there it is harmless:RouterLinkdeclaresariaCurrentValueas a real prop and consumes it.inertia/LinkBase.vue(and@inertiajs/vue3'sLink) 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 noto— all three renderedariacurrentvalue="page", client-side and in SSR output.Minimal repro:
3. Active matching uses a raw prefix against a query-carrying URL
inertia/Link.vue:119:usePage().urlin Inertia includes the query string, andstartsWithis a raw prefix test. Three consequences: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/.to="/inventory"reports active on/inventory-adjustments.The
vue-routervariant delegates this toRouterLink'sisActive/isExactActive, which are path- and segment-aware, so the Inertia variant is the odd one out.Suggested fix
...isLinkActive ? { 'aria-current': props.ariaCurrentValue } : {}to the twov-bindobjects ininertia/Link.vue's template (absent, not"false", when inactive)."ariaCurrentValue"to thereactiveOmitlist at:71.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.