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.
Before the checklist, the part you don't have to do:
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>.So the checklist below is short. It's the things only you know.
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:
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.
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.
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.
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.
q-header, q-footer, q-drawer and q-page render <header>, <footer>, <aside> and <main>. Two rules:
<main>. q-page already is one.q-toolbars, need an aria-label each (for example aria-label="Filters" on the right drawer) so screen reader users can tell them apart.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.
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.
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.
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:
$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.q-tab-panels and q-stepper only slide when you add animated. Bind it to the user's setting instead of hard-coding it.q-carousel with autoplay never pauses on hover or focus, so give it a pause button (WCAG 2.2.2).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.
The checklist catches the common mistakes. Testing catches the rest:
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.
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.