This episode covers accessibility and user experience in Nuxt: ARIA support, keyboard navigation and focus management, semantic HTML with accessible components, responsive design, and the basics of internationalization with @nuxtjs/i18n.

A good application can be used by everyone — including people using screen readers, navigating only with a keyboard, or on devices with small screens. Episode 17 covers accessibility and UX in Nuxt.
Accessibility isn't a bonus feature; it's part of quality. Many accessibility practices turn out to improve the UX for everyone: clear labels help everyone, a logical focus order makes navigation faster, and semantic HTML also boosts SEO.
ARIA provides additional information to screen readers. The golden rule: use semantic HTML elements first, ARIA only to complement:
<template>
<div
v-if="terbuka"
role="dialog"
aria-modal="true"
aria-labelledby="judul-dialog"
>
<h2 id="judul-dialog">Pilih Pengiriman</h2>
<button @click="terbuka = false">Tutup</button>
</div>
</template>role="dialog" and aria-modal="true" tell the screen reader this is a dialog that traps page interaction. aria-labelledby connects the dialog to its title.
All important interactions must be doable with a keyboard. Focus should move logically — and when a dialog closes, focus should return to the element that opened it:
function tutupDialog() {
terbuka.value = false
nextTick(() => {
tombolBuka.value?.focus()
})
}nextTick(() => tombolBuka.value?.focus()) returns focus to the opening button after the dialog closes. This pattern matters for both keyboard and screen reader users.
Use elements according to their meaning — header, nav, main, section, and footer — so screen readers can navigate the page structure. One frequently violated rule: buttons must be <button>, not <div> with a click event attached.
<template>
<button type="button" @click="tambahKeranjang">
Tambah ke Keranjang
</button>
</template><button> gets keyboard support for free: it can be focused and activated with Enter or Space. Replacing it with a div means rebuilding that behavior manually.
Connect every label to its input with the id attribute, and use aria-describedby to associate error messages:
<template>
<label for="email">Email</label>
<input id="email" v-model="email" type="email" aria-describedby="err-email" />
<p id="err-email" role="alert" v-if="errors.email">
{{ errors.email }}
</p>
</template>aria-describedby makes the screen reader read the error message connected to the input. Combine it with the patterns from episode 10 for a complete form.
Design for small screens first, then enhance for larger screens. Both Tailwind and CSS media queries support this pattern:
<template>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<KartuProduk v-for="item in produk" :key="item.id" v-bind="item" />
</div>
</template>grid-cols-1 md:grid-cols-3 shows one column on phones and three columns on medium screens and up. Make sure touch targets are large enough — at least 44 pixels — and text isn't too small to read.
Adaptive layouts adjust content, not just size. For example, hiding non-essential columns on small screens, or showing a condensed version. These are design decisions best tested with real users.
For multilingual support, use the official i18n module:
npm install @nuxtjs/i18nexport default defineNuxtConfig({
modules: ["@nuxtjs/i18n"],
i18n: {
locales: [{ code: "id", name: "Indonesia" }, { code: "en", name: "English" }],
defaultLocale: "id",
},
})Store translations in the i18n/locales folder, then use the t function:
const { t } = useI18n()
const judul = t("produk.tambahKeKeranjang")t("produk.tambahKeKeranjang") fetches the translation according to the active locale. For SEO, i18n also handles locale-prefixed URLs like /en/produk — supporting language inclusivity and search rankings at once.
Episode 17 makes your application friendly to all users: ARIA used correctly, keyboard navigation with focus management, semantic HTML as the foundation, responsive and adaptive layouts for every screen size, and the basics of internationalization with the i18n module.
Key takeaways:
<button>, not divs with click events.id and aria-describedby.In the next episode, episode 18, we will discuss architecture and modularization — feature-based project structure, module development and utility composables, separating concerns between UI, data, and logic, and how to keep a Nuxt codebase scalable.