Black Friday: 50% off all products for the first 50 customers. Only 10 left. Use code
40/50 claimed
See pricing →
November 12, 2026

Quasar Accessibility Checklist: Labels, Focus, ARIA

A Quasar accessibility checklist: what Quasar 2.35.2 does for you, and the labels, dialog names, icon buttons, live regions and contrast your app must add.
Quasar
Accessibility
Vue
ARIA

Quasar accessibility is better than most people assume. Since 2.25, Quasar's components render real <button>s and <form>s, wire labels to inputs, trap focus in dialogs and follow the WAI-ARIA keyboard patterns. But the Quasar docs are clear that accessibility is a contract: the framework can render a correct role="checkbox", and it can't know what your icon-only button means. This checklist is the app's half of that contract, item by item, with the Quasar 2.35.2 behavior each one relies on.

What Quasar accessibility gives you for free

Before the checklist, the part you don't have to do:

  • Native elements first. q-btn is a <button> (or <a> for links), q-form is a <form>, q-table is a <table>, and the layout renders <header>, <footer>, <aside> and <main>.
  • Keyboard support. Everything clickable responds to Enter and Space. Tabs, option groups, the date picker and the tree are a single Tab stop with arrow keys inside, as the ARIA patterns describe.
  • Escape closes one popup at a time, the top-most one.
  • Focus in dialogs and menus is moved in on open and returned to the opener on close.
  • A keyboard-only focus ring. Clicking with a mouse doesn't flash a ring; tabbing shows one.
  • Translated control labels. A field's clear button, pagination arrows and similar built-in controls get their names from the active Quasar language pack.

So the checklist below is short. It's the things only you know.

1. Every input has a visible label

Use the label prop. In 2.35.2, q-input renders a native <label for> pointing at the input, and also sets the label as the input's aria-label.

<q-input v-model="email" label="Work email" type="email" autocomplete="email" outlined />

What not to do:

  • A placeholder is not a label. It disappears as soon as someone types, and it's usually low-contrast.
  • A heading above the field isn't wired to it. If you use one, add aria-labelledby pointing at its id.
  • hint text isn't announced. Quasar's docs note that hint is purely visual. If it holds something users need ("At least 12 characters"), add your own element and reference it:
<q-input
  v-model="password"
  label="Password"
  type="password"
  outlined
  aria-describedby="password-rules"
  :rules="[value => value.length >= 12 || 'Use at least 12 characters']"
/>
<p id="password-rules" class="text-caption">At least 12 characters.</p>

Your aria-describedby is merged with Quasar's own: when a validation rule fails, the error message renders with role="alert", and the input gets aria-invalid plus a reference to the message. You get announced errors for free as long as you use rules.

2. Icon-only buttons have an aria-label

This is the most common failure in Quasar apps. A round button with only an icon has no accessible name, because every q-icon is aria-hidden="true", and a screen reader announces it as "button".

<q-btn round flat icon="delete" aria-label="Delete invoice" @click="remove">
  <q-tooltip>Delete</q-tooltip>
</q-btn>

q-btn has no aria-label prop, but the attribute falls through to the <button>. And the tooltip doesn't count: in 2.35.2, q-tooltip becomes the button's description (aria-describedby), never its name. Keep both, the label for screen readers and the tooltip for sighted mouse users.

The same goes for q-fab, icon-only q-route-tabs and icon toggles. If the only thing inside is an icon, add an aria-label.

3. Every dialog has a name

q-dialog handles the hard parts: role="dialog" and aria-modal, focus moved in when it opens, focus that wanders out pulled back in, focus returned to the opener on close. What it can't do is name itself. Without a name, it's announced as an unnamed dialog.

<q-dialog v-model="open" aria-labelledby="delete-title">
  <q-card style="min-width: 320px">
    <q-card-section>
      <div id="delete-title" class="text-h6">Delete this invoice?</div>
      <p>This can't be undone.</p>
    </q-card-section>
    <q-card-actions align="right">
      <q-btn flat label="Cancel" v-close-popup autofocus />
      <q-btn unelevated color="negative" label="Delete" @click="remove" />
    </q-card-actions>
  </q-card>
</q-dialog>

aria-labelledby falls through to the role="dialog" element. The autofocus on Cancel tells q-dialog where to put focus when it opens. For a destructive action, start on the safe choice.

Leave no-focus, no-refocus and allow-focus-outside alone unless you're managing focus yourself. They turn off exactly the behavior that makes the dialog usable from a keyboard.

4. Meaningful icons are exposed, decorative ones aren't

Every q-icon is hidden from assistive technology by default, which is right for most icons: the text next to them already says it. For the rare icon that carries meaning on its own, such as a status icon in a table cell, opt back in:

<q-icon name="error" color="negative" aria-hidden="false" role="img" aria-label="Payment failed" />

For an icon inside a button or link, label the button, not the icon.

5. Landmarks are labeled, and there's only one main

q-header, q-footer, q-drawer and q-page render <header>, <footer>, <aside> and <main>. Two rules:

  • Don't add your own <main>. q-page already is one.
  • Label repeated landmarks and toolbars. Two drawers, or a page with several q-toolbars, need an aria-label each (for example aria-label="Filters" on the right drawer) so screen reader users can tell them apart.

6. Menus declare what they are

q-menu deliberately has no role, because it can hold anything. If its content is a list of commands, say so on the list:

<q-btn flat icon="more_vert" aria-label="Invoice actions" aria-haspopup="menu">
  <q-menu>
    <q-list role="menu" style="min-width: 160px">
      <q-item clickable v-close-popup @click="download">
        <q-item-section>Download PDF</q-item-section>
      </q-item>
      <q-item clickable v-close-popup @click="duplicate">
        <q-item-section>Duplicate</q-item-section>
      </q-item>
    </q-list>
  </q-menu>
</q-btn>

The items then take the menuitem role from the list, and q-menu keeps aria-expanded on the button for you. aria-haspopup is left to you when the role is declared on the list, as it is here.

7. Loading states are announced

q-spinner, q-inner-loading and q-skeleton are visual only. If a state change matters ("Saving…", "3 results"), put it in a live region:

<div role="status" class="sr-only">{{ saving ? 'Saving…' : statusMessage }}</div>

sr-only here is your own visually-hidden class. Quasar's q-badge is already a polite live region, and q-banner is role="alert", so a banner you insert after a failed request is announced on its own.

8. Brand colors pass contrast, in both modes

The Quasar docs put this squarely on the app: there's no contrast enforcement, in light or dark mode. Text needs 4.5:1, large text and UI parts 3:1. Several of the stock brand colors already miss that with white text: positive, info and warning all do, and accent and negative fail as text on the default dark page. The Quasar color palette contrast post has the measured numbers and the fixes.

9. Motion can be turned down

Only Quasar's animation CSS helpers respect prefers-reduced-motion. Component transitions and the ripple don't, so if your app moves a lot, tone it down yourself:

  • The ripple. The Quasar docs suggest $q.config.ripple = false. In 2.35.2 the ripple directive reads that setting when a component mounts, so set it early (before the UI renders), and only when window.matchMedia('(prefers-reduced-motion: reduce)') matches. On a single button, :ripple="false" does the same.
  • Transitions. Components such as q-tab-panels and q-stepper only slide when you add animated. Bind it to the user's setting instead of hard-coding it.
  • Autoplay. q-carousel with autoplay never pauses on hover or focus, so give it a pause button (WCAG 2.2.2).

10. Zoom isn't disabled

Old Quasar scaffolds shipped a viewport meta with user-scalable=no, maximum-scale=1. That fails WCAG 1.4.4 and every automated audit. Check index.html and use width=device-width, initial-scale=1. If iOS zooming into 14px inputs bothers you, raise $input-font-size to 16px rather than locking zoom again.

How to test Quasar accessibility

The checklist catches the common mistakes. Testing catches the rest:

  • Keyboard only. Unplug the mouse and walk through sign-up, the main task and checkout. Can you reach everything, see where focus is, and get back out of every dialog?
  • A screen reader pass. VoiceOver is built into macOS and iOS; NVDA is free on Windows.
  • Automated checks in CI. axe or Lighthouse catch missing names, contrast failures and invalid ARIA. They can't tell you whether a label makes sense, which is what the manual passes are for.

Common questions about Quasar accessibility

Is Quasar accessible out of the box? The components are: native elements, ARIA patterns, keyboard support and focus management since 2.25. Your app still has to supply names, labels, landmarks and contrast.

Do I need aria-label on a q-btn with a label? No. The visible label is the accessible name. Only icon-only buttons need one.

Does q-dialog trap focus? It moves focus in on open, pulls it back while the dialog is modal, and returns it to the opener on close. q-drawer in overlay mode does not trap focus.

What about QSelect? It implements the full combobox pattern with typeahead. Give it a label like any other field.

Check your colors next

Item 8 is the one you can't eyeball. Run your eight brand colors through the free Brand Color Contrast Checker and see which pairs fail in light and dark mode.