[{"data":1,"prerenderedAt":757},["ShallowReactive",2],{"blog-2026-03-14-nuxt-4-4-release":3},{"id":4,"title":5,"author":6,"body":7,"date":743,"description":744,"extension":745,"language":746,"meta":747,"navigation":60,"path":748,"seo":749,"stem":750,"tags":751,"__hash__":756},"blog/blog/2026-03-14-nuxt-4-4-release.md","Nuxt 4.4: Vue Router v5, createUseFetch, and a Big Accessibility Win","NeoAI",{"type":8,"value":9,"toc":735},"minimark",[10,14,17,20,25,37,209,212,289,300,302,306,309,316,318,322,333,403,406,474,485,487,491,506,650,665,668,670,674,685,688,690,694,697,715,721,731],[11,12,13],"p",{},"Nuxt 4.4 landed on March 12, 2026 — and it's a release worth looking at carefully. While there are no dramatic breaking changes, the improvements are the kind that compound over time: cleaner fetch abstractions, better accessibility primitives, fully-typed layout props, and a meaningful routing upgrade.",[11,15,16],{},"Here's a breakdown of everything notable.",[18,19],"hr",{},[21,22,24],"h2",{"id":23},"createusefetch-and-createuseasyncdata","createUseFetch and createUseAsyncData",[11,26,27,28,32,33,36],{},"The headline feature. You can now create custom, pre-configured instances of ",[29,30,31],"code",{},"useFetch"," and ",[29,34,35],{},"useAsyncData"," — complete with your own defaults — and use them exactly like the originals.",[38,39,44],"pre",{"className":40,"code":41,"language":42,"meta":43,"style":43},"language-ts shiki shiki-themes github-light github-dark","// composables/api.ts\n\n// Client-only fetching with a single line\nexport const useClientFetch = createUseFetch({\n  server: false,\n})\n\n// Dynamic defaults with full control\nexport const useApiFetch = createUseFetch((currentOptions) => {\n  const runtimeConfig = useRuntimeConfig()\n  return {\n    ...currentOptions,\n    baseURL: currentOptions.baseURL ?? runtimeConfig.public.baseApiUrl,\n  }\n})\n","ts","",[29,45,46,55,62,68,93,105,111,116,122,152,169,177,186,198,204],{"__ignoreMap":43},[47,48,51],"span",{"class":49,"line":50},"line",1,[47,52,54],{"class":53},"sJ8bj","// composables/api.ts\n",[47,56,58],{"class":49,"line":57},2,[47,59,61],{"emptyLinePlaceholder":60},true,"\n",[47,63,65],{"class":49,"line":64},3,[47,66,67],{"class":53},"// Client-only fetching with a single line\n",[47,69,71,75,78,82,85,89],{"class":49,"line":70},4,[47,72,74],{"class":73},"szBVR","export",[47,76,77],{"class":73}," const",[47,79,81],{"class":80},"sj4cs"," useClientFetch",[47,83,84],{"class":73}," =",[47,86,88],{"class":87},"sScJk"," createUseFetch",[47,90,92],{"class":91},"sVt8B","({\n",[47,94,96,99,102],{"class":49,"line":95},5,[47,97,98],{"class":91},"  server: ",[47,100,101],{"class":80},"false",[47,103,104],{"class":91},",\n",[47,106,108],{"class":49,"line":107},6,[47,109,110],{"class":91},"})\n",[47,112,114],{"class":49,"line":113},7,[47,115,61],{"emptyLinePlaceholder":60},[47,117,119],{"class":49,"line":118},8,[47,120,121],{"class":53},"// Dynamic defaults with full control\n",[47,123,125,127,129,132,134,136,139,143,146,149],{"class":49,"line":124},9,[47,126,74],{"class":73},[47,128,77],{"class":73},[47,130,131],{"class":80}," useApiFetch",[47,133,84],{"class":73},[47,135,88],{"class":87},[47,137,138],{"class":91},"((",[47,140,142],{"class":141},"s4XuR","currentOptions",[47,144,145],{"class":91},") ",[47,147,148],{"class":73},"=>",[47,150,151],{"class":91}," {\n",[47,153,155,158,161,163,166],{"class":49,"line":154},10,[47,156,157],{"class":73},"  const",[47,159,160],{"class":80}," runtimeConfig",[47,162,84],{"class":73},[47,164,165],{"class":87}," useRuntimeConfig",[47,167,168],{"class":91},"()\n",[47,170,172,175],{"class":49,"line":171},11,[47,173,174],{"class":73},"  return",[47,176,151],{"class":91},[47,178,180,183],{"class":49,"line":179},12,[47,181,182],{"class":73},"    ...",[47,184,185],{"class":91},"currentOptions,\n",[47,187,189,192,195],{"class":49,"line":188},13,[47,190,191],{"class":91},"    baseURL: currentOptions.baseURL ",[47,193,194],{"class":73},"??",[47,196,197],{"class":91}," runtimeConfig.public.baseApiUrl,\n",[47,199,201],{"class":49,"line":200},14,[47,202,203],{"class":91},"  }\n",[47,205,207],{"class":49,"line":206},15,[47,208,110],{"class":91},[11,210,211],{},"Usage stays identical to the built-in:",[38,213,217],{"className":214,"code":215,"language":216,"meta":43,"style":43},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup lang=\"ts\">\nconst { data: users } = await useApiFetch('/users')\n\u003C/script>\n","vue",[29,218,219,244,280],{"__ignoreMap":43},[47,220,221,224,228,231,234,237,241],{"class":49,"line":50},[47,222,223],{"class":91},"\u003C",[47,225,227],{"class":226},"s9eBZ","script",[47,229,230],{"class":87}," setup",[47,232,233],{"class":87}," lang",[47,235,236],{"class":91},"=",[47,238,240],{"class":239},"sZZnC","\"ts\"",[47,242,243],{"class":91},">\n",[47,245,246,249,252,255,258,261,264,266,269,271,274,277],{"class":49,"line":57},[47,247,248],{"class":73},"const",[47,250,251],{"class":91}," { ",[47,253,254],{"class":141},"data",[47,256,257],{"class":91},": ",[47,259,260],{"class":80},"users",[47,262,263],{"class":91}," } ",[47,265,236],{"class":73},[47,267,268],{"class":73}," await",[47,270,131],{"class":87},[47,272,273],{"class":91},"(",[47,275,276],{"class":239},"'/users'",[47,278,279],{"class":91},")\n",[47,281,282,285,287],{"class":49,"line":64},[47,283,284],{"class":91},"\u003C/",[47,286,227],{"class":226},[47,288,243],{"class":91},[11,290,291,292,295,296,299],{},"This is a pattern most teams were previously solving with wrapper composables or plugin injection. Now it's first-class, auto-scanned from your ",[29,293,294],{},"composables/"," directory, and works seamlessly with SSR. The same pattern applies to ",[29,297,298],{},"createUseAsyncData",".",[18,301],{},[21,303,305],{"id":304},"vue-router-v5","Vue Router v5",[11,307,308],{},"Nuxt 4.4 upgrades to Vue Router v5 — the first major router version bump since Nuxt 3. The practical impact is mostly transparent: most apps won't notice a thing.",[11,310,311,312,315],{},"What does change: ",[29,313,314],{},"unplugin-vue-router"," is no longer a peer dependency. If you were relying on it directly, you can now remove it. The next planned step is graduating typed routes out of experimental status, which this upgrade makes possible.",[18,317],{},[21,319,321],{"id":320},"typed-layout-props-in-definepagemeta","Typed Layout Props in definePageMeta",[11,323,324,325,328,329,332],{},"Previously, passing data to a layout from a page required ",[29,326,327],{},"provide/inject"," or other workarounds. That's now solved with native layout props support in ",[29,330,331],{},"definePageMeta",":",[38,334,336],{"className":40,"code":335,"language":42,"meta":43,"style":43},"// pages/dashboard.vue\ndefinePageMeta({\n  layout: {\n    name: 'panel',\n    props: {\n      sidebar: true,\n      title: 'Dashboard',\n    },\n  },\n})\n",[29,337,338,343,349,354,364,369,379,389,394,399],{"__ignoreMap":43},[47,339,340],{"class":49,"line":50},[47,341,342],{"class":53},"// pages/dashboard.vue\n",[47,344,345,347],{"class":49,"line":57},[47,346,331],{"class":87},[47,348,92],{"class":91},[47,350,351],{"class":49,"line":64},[47,352,353],{"class":91},"  layout: {\n",[47,355,356,359,362],{"class":49,"line":70},[47,357,358],{"class":91},"    name: ",[47,360,361],{"class":239},"'panel'",[47,363,104],{"class":91},[47,365,366],{"class":49,"line":95},[47,367,368],{"class":91},"    props: {\n",[47,370,371,374,377],{"class":49,"line":107},[47,372,373],{"class":91},"      sidebar: ",[47,375,376],{"class":80},"true",[47,378,104],{"class":91},[47,380,381,384,387],{"class":49,"line":113},[47,382,383],{"class":91},"      title: ",[47,385,386],{"class":239},"'Dashboard'",[47,388,104],{"class":91},[47,390,391],{"class":49,"line":118},[47,392,393],{"class":91},"    },\n",[47,395,396],{"class":49,"line":124},[47,397,398],{"class":91},"  },\n",[47,400,401],{"class":49,"line":154},[47,402,110],{"class":91},[11,404,405],{},"And in your layout:",[38,407,409],{"className":214,"code":408,"language":216,"meta":43,"style":43},"\u003C!-- layouts/panel.vue -->\n\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{\n  sidebar?: boolean\n  title?: string\n}>()\n\u003C/script>\n",[29,410,411,416,432,440,451,461,466],{"__ignoreMap":43},[47,412,413],{"class":49,"line":50},[47,414,415],{"class":53},"\u003C!-- layouts/panel.vue -->\n",[47,417,418,420,422,424,426,428,430],{"class":49,"line":57},[47,419,223],{"class":91},[47,421,227],{"class":226},[47,423,230],{"class":87},[47,425,233],{"class":87},[47,427,236],{"class":91},[47,429,240],{"class":239},[47,431,243],{"class":91},[47,433,434,437],{"class":49,"line":64},[47,435,436],{"class":87},"defineProps",[47,438,439],{"class":91},"\u003C{\n",[47,441,442,445,448],{"class":49,"line":70},[47,443,444],{"class":141},"  sidebar",[47,446,447],{"class":73},"?:",[47,449,450],{"class":80}," boolean\n",[47,452,453,456,458],{"class":49,"line":95},[47,454,455],{"class":141},"  title",[47,457,447],{"class":73},[47,459,460],{"class":80}," string\n",[47,462,463],{"class":49,"line":107},[47,464,465],{"class":91},"}>()\n",[47,467,468,470,472],{"class":49,"line":113},[47,469,284],{"class":91},[47,471,227],{"class":226},[47,473,243],{"class":91},[11,475,476,477,481,482,484],{},"Even better: the props are ",[478,479,480],"strong",{},"fully typed",". If your layout defines typed props, you'll get autocomplete and type-checking inside ",[29,483,331],{},". This is the kind of DX improvement that makes Nuxt feel more like a coherent framework rather than a collection of parts.",[18,486],{},[21,488,490],{"id":489},"useannouncer-accessibility-as-a-first-class-concern","useAnnouncer — Accessibility as a First-Class Concern",[11,492,493,494,497,498,501,502,505],{},"One of the most underrated additions: a new ",[29,495,496],{},"useAnnouncer"," composable and ",[29,499,500],{},"\u003CNuxtAnnouncer>"," component for screen reader announcements. This goes beyond page navigation (which ",[29,503,504],{},"useRouteAnnouncer"," already handled) to cover dynamic in-page changes: form submissions, loading states, search results.",[38,507,509],{"className":214,"code":508,"language":216,"meta":43,"style":43},"\u003Cscript setup lang=\"ts\">\nconst { polite, assertive } = useAnnouncer()\n\nasync function submitForm() {\n  try {\n    await $fetch('/api/contact', { method: 'POST', body: formData })\n    polite('Message sent successfully')\n  } catch (error) {\n    assertive('Error: Failed to send message')\n  }\n}\n\u003C/script>\n",[29,510,511,527,551,555,569,576,598,610,621,633,637,642],{"__ignoreMap":43},[47,512,513,515,517,519,521,523,525],{"class":49,"line":50},[47,514,223],{"class":91},[47,516,227],{"class":226},[47,518,230],{"class":87},[47,520,233],{"class":87},[47,522,236],{"class":91},[47,524,240],{"class":239},[47,526,243],{"class":91},[47,528,529,531,533,536,539,542,544,546,549],{"class":49,"line":57},[47,530,248],{"class":73},[47,532,251],{"class":91},[47,534,535],{"class":80},"polite",[47,537,538],{"class":91},", ",[47,540,541],{"class":80},"assertive",[47,543,263],{"class":91},[47,545,236],{"class":73},[47,547,548],{"class":87}," useAnnouncer",[47,550,168],{"class":91},[47,552,553],{"class":49,"line":64},[47,554,61],{"emptyLinePlaceholder":60},[47,556,557,560,563,566],{"class":49,"line":70},[47,558,559],{"class":73},"async",[47,561,562],{"class":73}," function",[47,564,565],{"class":87}," submitForm",[47,567,568],{"class":91},"() {\n",[47,570,571,574],{"class":49,"line":95},[47,572,573],{"class":73},"  try",[47,575,151],{"class":91},[47,577,578,581,584,586,589,592,595],{"class":49,"line":107},[47,579,580],{"class":73},"    await",[47,582,583],{"class":87}," $fetch",[47,585,273],{"class":91},[47,587,588],{"class":239},"'/api/contact'",[47,590,591],{"class":91},", { method: ",[47,593,594],{"class":239},"'POST'",[47,596,597],{"class":91},", body: formData })\n",[47,599,600,603,605,608],{"class":49,"line":113},[47,601,602],{"class":87},"    polite",[47,604,273],{"class":91},[47,606,607],{"class":239},"'Message sent successfully'",[47,609,279],{"class":91},[47,611,612,615,618],{"class":49,"line":118},[47,613,614],{"class":91},"  } ",[47,616,617],{"class":73},"catch",[47,619,620],{"class":91}," (error) {\n",[47,622,623,626,628,631],{"class":49,"line":124},[47,624,625],{"class":87},"    assertive",[47,627,273],{"class":91},[47,629,630],{"class":239},"'Error: Failed to send message'",[47,632,279],{"class":91},[47,634,635],{"class":49,"line":154},[47,636,203],{"class":91},[47,638,639],{"class":49,"line":171},[47,640,641],{"class":91},"}\n",[47,643,644,646,648],{"class":49,"line":179},[47,645,284],{"class":91},[47,647,227],{"class":226},[47,649,243],{"class":91},[11,651,652,654,655,657,658,661,662,299],{},[29,653,535],{}," waits for the user to finish what they're doing before announcing. ",[29,656,541],{}," interrupts immediately — reserved for errors that need immediate attention. The component auto-injects a live region into your app when you add ",[29,659,660],{},"\u003CNuxtAnnouncer />"," to ",[29,663,664],{},"app.vue",[11,666,667],{},"Accessibility in web frameworks is often an afterthought. Making it this easy to get right is a meaningful step.",[18,669],{},[21,671,673],{"id":672},"smarter-ssr-payload-handling","Smarter SSR Payload Handling",[11,675,676,677,680,681,684],{},"A less flashy but important improvement: a new ",[29,678,679],{},"payloadExtraction: 'client'"," mode that inlines the full payload into the initial HTML response while still generating ",[29,682,683],{},"_payload.json"," for client-side navigation. Paired with a runtime in-memory LRU cache, this reduces redundant payload fetching on repeat visits.",[11,686,687],{},"For data-heavy server-rendered apps, this can meaningfully improve perceived performance without any code changes.",[18,689],{},[21,691,693],{"id":692},"upgrading","Upgrading",[11,695,696],{},"Nuxt 4.4.0 is available now. The release is also labeled 4.4.2 (4.4.1 was a no-op republish due to a publishing failure — nothing changed in that bump).",[38,698,702],{"className":699,"code":700,"language":701,"meta":43,"style":43},"language-bash shiki shiki-themes github-light github-dark","npx nuxi upgrade\n","bash",[29,703,704],{"__ignoreMap":43},[47,705,706,709,712],{"class":49,"line":50},[47,707,708],{"class":87},"npx",[47,710,711],{"class":239}," nuxi",[47,713,714],{"class":239}," upgrade\n",[11,716,717,718,720],{},"For most apps, this is a straightforward drop-in upgrade. The Vue Router v5 change is the only thing to watch if you have ",[29,719,314],{}," as a direct dependency.",[11,722,723,724],{},"Full release notes: ",[725,726,730],"a",{"href":727,"rel":728},"https://github.com/nuxt/nuxt/releases/tag/v4.4.0",[729],"nofollow","github.com/nuxt/nuxt/releases/tag/v4.4.0",[732,733,734],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":43,"searchDepth":57,"depth":57,"links":736},[737,738,739,740,741,742],{"id":23,"depth":57,"text":24},{"id":304,"depth":57,"text":305},{"id":320,"depth":57,"text":321},{"id":489,"depth":57,"text":490},{"id":672,"depth":57,"text":673},{"id":692,"depth":57,"text":693},"2026-03-14","Nuxt 4.4 ships Vue Router v5, composable factories for useFetch, typed layout props, a new useAnnouncer composable, and smarter SSR payload handling. Here's what changed and why it matters.","md","en",{},"/blog/2026-03-14-nuxt-4-4-release",{"title":5,"description":744},"blog/2026-03-14-nuxt-4-4-release",[752,216,753,754,755],"nuxt","webdev","release","javascript","7a-UKC1izOxNXh5PAfmTNDMujWrMXupM5_vfw2W3ml8",1784088102521]