Accessibility (often abbreviated “a11y”) means your website or app can be used by everyone — including people relying on screen readers, keyboard-only navigation, voice control or other assistive technology (AT). Quasar components ship with a substantial accessibility layer: semantic markup, WAI-ARIA attributes, keyboard interaction patterns and focus management. This page describes what the framework does for you, what it deliberately leaves to your app, and — component by component — where to find the details.
Accessibility is a contract between the framework and your app. Quasar can render a correct role="checkbox" with aria-checked and Space/Enter handling, but it cannot know what an icon-only button means, whether your brand colors have enough contrast, or which landmark a drawer represents. The component matrix below tells you exactly which side of the contract each piece lives on.
What Quasar provides
Semantic markup
Components render native HTML elements wherever one exists for the job — a real <button> (QBtn), <a href> (router links everywhere), <form> (QForm), <table> (QTable), <hr> (QSeparator), and the full landmark set from the QLayout family: <header>, <footer>, <aside> and <main>. Native elements carry their semantics, states and keyboard behavior for free, which is always more robust than re-creating them with ARIA.
Where the visual design requires custom elements, components declare the matching ARIA role and manage its required states: role="checkbox"/"radio"/"switch" with aria-checked (including mixed), role="slider"/"spinbutton" with aria-valuemin/max/now, role="progressbar", role="tablist"/"tab"/"tabpanel", role="tree"/"treeitem" (with aria-level/aria-setsize/aria-posinset compensating for virtual scrolling), role="combobox" + "listbox"/"option" (QSelect), and role="separator" with full keyboard resizing (QSplitter).
Some containers deliberately claim no role, because they can host arbitrary content and a wrong role would produce invalid markup. QMenu is the canonical example: ARIA’s menu role permits nothing but menu items as children, so QMenu stays neutral and you declare role="menu" on the QList inside it when the content really is a list of commands. QItems then derive their roles from context — menuitem inside a declared menu, listitem inside a list, button when clickable, a plain link when they navigate. See the QList/QItem Accessibility section for the full derivation contract.
Decorative elements are hidden from AT: every QIcon renders aria-hidden="true" by default (interactive icons like a field’s clear button opt back in with a role and a localized label), and backdrops, virtual-scroll padding, custom scrollbars and similar plumbing are all aria-hidden.
Keyboard support
- Activation: everything clickable is keyboard-activatable. QBtn even synthesizes Space activation on link-shaped (
<a>) buttons, which natively respond only to Enter. Clickable QItems, chips, expansion headers, sortable table headers and stepper headers all handle Enter/Space. - Composite widgets use roving tabindex — the whole widget is a single Tab stop and arrow keys move within it, per the WAI-ARIA APG: QTabs, QOptionGroup (radio mode), QRating, QDate’s calendar, QCarousel’s navigation, QEditor’s toolbar and QTree.
- Value widgets respond to arrow keys (QSlider, QRange, QKnob, QSplitter, QTime’s spinbuttons), most with Home/End and PageUp/PageDown variants; QSelect implements the full combobox keyboard contract including typeahead.
- Escape dismissal works as a chain: only the top-most open popup (dialog, menu, tooltip, drawer in overlay mode) reacts, closing popups one at a time. Persistent popups shake or ignore instead of closing.
- Horizontal arrow keys are RTL-aware throughout (RTL support) — tabs, radio groups, sliders, date navigation, editor toolbar and splitter all mirror direction under RTL language packs.
Note that Escape handling and focus recapture are desktop-mode behaviors; on mobile platforms, dismissal is wired to the platform back button through Quasar’s History plugin (Capacitor/Cordova builds).
Focus management
- Dialogs record the element that opened them, move focus into the dialog when it renders (honoring
autofocus/data-autofocus), recapture focus that strays out while modal, and restore focus to the opener on close. - Menus do the same, with one refinement: Tab from the menu’s last focusable element (or Shift+Tab from the first) closes the menu and continues tabbing from the anchor — keyboard focus can never leak into the void behind a portal.
- Focus moves are queued while popup transitions play, so competing focus intents can’t race; a menu opened inside a modal dialog renders inside the dialog’s element so
aria-modaldoesn’t hide it from AT. - QForm focuses the first invalid field after failed validation.
- The focus ring is keyboard-only: after mouse or touch interaction, components park focus on an invisible helper so no ring flashes; keyboard focus keeps a visible indicator. This is built in — no
:focus-visiblepolyfill needed. (The built-in indicator styling targets desktop mode; hybrid touch-plus-keyboard apps may want their own focus styles.)
Screen reader labels and language packs
Built-in control labels — a field’s clear button, a chip’s remove icon, expand/collapse arrows, pagination’s first/prev/next/last, QDate’s month/year navigation, carousel arrows, the editor toolbar — come from the active Quasar Language Pack and are announced in your app’s language automatically. If you author a custom language pack, cover these keys (several, like label.expand, are functions); screen reader users get whatever the pack provides.
ARIA attributes fall through
Any aria-* attribute (or role) you place on a Quasar component reaches the semantically correct element: on form fields it lands on the native <input>/focus target, on QDialog it lands on the role="dialog" element, and so on. Your attributes win over generated ones (with aria-describedby on fields being merged, not clobbered). This is the standard mechanism for everything in the “your responsibility” list below — no special props needed.
What your app is responsible for
Accessible names. Nothing can generate a meaningful name for you. Provide aria-label (or visible text) for: icon-only QBtns and FABs; alt for QImg; title for QVideo; aria-label/aria-labelledby for QDialog, QDrawer, QToolbar, QOptionGroup and any role="navigation"/role="group" container you want distinguishable. Multiple unlabeled landmarks or toolbars are a WCAG failure. Note that a QTooltip does not name its anchor — it only describes it — so an icon-only control needs its own aria-label even when it carries one.
Popup semantics on custom triggers. QBtnDropdown and QSelect fully wire their trigger ARIA (aria-expanded, conditional aria-controls), and since v2.25 QMenu maintains aria-expanded (plus aria-haspopup for a role declared on the QMenu itself) on its anchor too. It can only do so when that anchor is a control ARIA allows the state on, so an anchor of yours needs to be a <button>, a link or an element with a widget role — a plain <div> gets nothing, by design. Declaring the role on the QList inside the popup, the recommended shape, still leaves aria-haspopup to you — on a QMenu anchor as a plain attribute, on QBtnDropdown through its toggle-aria-haspopup prop. See QMenu and QBtnDropdown.
Landmark structure. The QLayout family gives you one of each landmark for free — do not add your own <main> (QPage already is one), and label multiple drawers or navigation areas so they can be told apart.
Color contrast. Quasar ships no contrast enforcement, in light or dark mode. Check your brand palette against WCAG 2.2 contrast minima (4.5:1 for normal text) with a tool like the WebAIM contrast checker — in both themes if you support both.
Reduced motion. Only the animation CSS helpers respect prefers-reduced-motion. Component transitions, the ripple and scroll-driven effects (QParallax) do not — motion-sensitive users need you to tone these down yourself (e.g. $q.config.ripple = false, transition props, disabling autoplay). QCarousel’s autoplay never pauses on hover or focus; provide a pause control if you use it (WCAG 2.2.2).
Announcing async state. Loading indicators — QSpinner, QInnerLoading, QSkeleton, QAjaxBar, QUploader’s per-file status — are visual only. Wrap or accompany them with a live region (role="status" plus a short text like “Loading…”) when the state change matters to the user, and hide purely decorative placeholders with aria-hidden="true".
Keyboard alternatives for gestures. Swipe-to-act (QSlideItem), pull-to-refresh and touch panning have no keyboard path. Offer a parallel affordance — visible buttons, a context menu, or QPullToRefresh’s trigger() method wired to a button.
Viewport zoom. WCAG 1.4.4 requires text to remain resizable up to 200%. Quasar CLI scaffolds allow pinch zoom in web modes (width=device-width, initial-scale=1); apps scaffolded earlier shipped a viewport meta with user-scalable=no, maximum-scale=1, which fails every automated audit — update the <meta name="viewport"> in your index.html to match. Cordova/Capacitor builds intentionally keep a fixed viewport for a native app feel; OS-level zoom (iOS Zoom, Android magnification) covers accessibility there. One side effect of an unlocked viewport: iOS Safari auto-zooms the page when an editable element with a font size below 16px receives focus. Three surfaces sit below that threshold, and each takes its size from a different place — QField-based controls (QInput, QSelect, QFile, QPagination) from $input-font-size (14px), QColorPicker’s tune-tab inputs from $color-picker-tune-tab-input-font-size (11px), and QEditor’s content, which declares no size of its own and so inherits $body-font-size (14px). If the zoom disrupts your design, raise the Sass variables covering the surfaces you actually use — $input-font-size: 16px on its own still leaves QEditor and QColorPicker zooming — or scope a 16px font size to those elements, rather than re-disabling zoom.
Testing. No framework replaces testing. Walk your critical flows with keyboard only (is everything reachable? is focus visible? can you get back out?); run a screen reader pass (VoiceOver ships with macOS/iOS, NVDA is free on Windows); and add automated checks with axe or Lighthouse to CI — they catch the mechanical failures (names, contrast, ARIA validity) so your manual passes can focus on flow.
Components at a glance
Each component name links to the Accessibility section of its documentation page, which describes the exact behavior — including its limits — and what you should add on top.
Buttons
| Component | Built-in behavior | Keyboard |
|---|---|---|
| QBtn | Native <button> or <a> (role="button" derived where needed); aria-disabled; progressbar ARIA while loading with percentage | Enter/Space, including Space on link buttons |
| QBtnDropdown | Disclosure pattern: aria-expanded, aria-controls (while the popup exists), localized expand/collapse label, opt-in toggle-aria-haspopup | Inherited from QBtn + QMenu |
| QBtnGroup | Visual grouping only — add role="group" + aria-label yourself | Each button is its own Tab stop |
| QBtnToggle | aria-pressed per option; per-option attrs for labels | Enter/Space per button |
| QFab | aria-expanded/aria-controls on the trigger; closed actions hidden from AT; focus returns to trigger | Enter/Space opens and activates |
Navigation
| Component | Built-in behavior | Keyboard |
|---|---|---|
| QTabs | role="tablist"/"tab" with aria-selected, aria-orientation | Roving tabindex; arrows (RTL-aware), Home/End; explicit activation |
| QTabPanels | role="tabpanel", tabindex="0"; tab↔panel id wiring is manual (documented) | Panel reachable via Tab |
| QBreadcrumbs | Native links; no landmark/aria-current — wrap in <nav> and mark the current page yourself | Native link behavior |
| QPagination | Named role="navigation"; localized first/prev/next/last labels; aria-current on the active page | Enter/Space on buttons; Enter commits input mode |
| QStepper | aria-current="step"; each step a labeled group; navigable headers exposed as buttons (disabled ones as aria-disabled buttons) | Enter/Space on navigable headers |
| QToolbar / QBar | role="toolbar" — name it via aria-label when a page has several | Children are individual Tab stops |
Form fields
| Component | Built-in behavior | Keyboard |
|---|---|---|
| QField / QInput | <label for> wiring; errors announced via role="alert" with aria-invalid/aria-errormessage/aria-describedby (only while the message renders); accessible clear button | Native editing; Enter/Space on the clear button |
| QSelect | Full combobox pattern: role="combobox" + aria-expanded/aria-controls/aria-activedescendant, role="listbox"/"option" with aria-selected and virtual-scroll-aware aria-setsize/posinset | Arrows open/navigate, typeahead, Home/End, PageUp/PageDown, Enter selects, Esc closes |
| QForm | Native <form>; focuses the first invalid field on failed validation | Native submit |
| QEditor | role="textbox" + aria-multiline; toolbar follows the APG toolbar pattern with localized labels | Toolbar roving tabindex, arrows/Home/End; Ctrl formatting shortcuts |
| QFile | Field frame + validation ARIA; keyboard-openable picker | Enter/Space opens the picker |
| QUploader | Real buttons with localized names, progressbar ARIA — but you must still announce status changes | Native button activation |
Form controls
| Component | Built-in behavior | Keyboard |
|---|---|---|
| QCheckbox | role="checkbox", three-state aria-checked (incl. mixed), aria-label from label | Enter/Space toggles |
| QRadio | role="radio" + aria-checked; group semantics come from QOptionGroup | Enter/Space selects |
| QToggle | role="switch" + aria-checked | Enter/Space toggles |
| QOptionGroup | role="radiogroup"/"group"; APG radio-group pattern | Roving tabindex; arrows select (RTL-aware), skipping disabled options |
| QSlider / QRange | role="slider" with aria-valuemin/max/now, aria-orientation, aria-readonly (QRange: one named slider per thumb) | Arrows step (RTL/vertical-aware), PageUp/PageDown ×10 |
| QRating | Radio-group pattern per star with per-star labels (icon-aria-label) | Roving tabindex; arrows move, Enter/Space select |
| QKnob | role="slider" with value ARIA on the focusable element | Arrows step, PageUp/PageDown ×10 |
| QColor | Tune tab (native inputs + sliders) is the accessible path, with localized tab/field/slider names; palette/spectrum are pointer-only | Native inputs + slider keys on the Tune tab |
| QDate | Day buttons with full-date labels, aria-pressed selection, aria-current="date" for today; localized navigation | Roving tabindex; arrows cross months, Home/End, PageUp/PageDown (+Shift for year) |
| QTime | Header role="spinbutton" per unit with localized labels and value ARIA; clock face is a pointer-only visualization | Arrows step, Home/End, direct digit entry |
Popups
| Component | Built-in behavior | Keyboard |
|---|---|---|
| QDialog | role="dialog" + aria-modal; focus moved in on open, contained while modal, restored on close — name it via aria-label/aria-labelledby | Esc dismisses (persistent dialogs shake instead) |
| QMenu | Role-less popup by design; declare role="menu" on the QList inside; aria-expanded on a control anchor; focus restored on close | Esc closes; Tab past the edge closes and continues from the anchor |
| QTooltip | Describes its anchor via aria-describedby while shown — never names it; role="tooltip"; shows on keyboard focus | Esc dismisses without moving focus (WCAG 1.4.13) |
| QPopupProxy | Switches between QMenu and QDialog by screen size — semantics follow the rendered component | Delegated |
| QPopupEdit | QMenu-based edit surface; localized Set/Cancel buttons; never commits silently on dismissal | Esc cancels; wire Enter-to-save yourself |
Lists and data
| Component | Built-in behavior | Keyboard |
|---|---|---|
| QList / QItem | Context-derived roles: list/listitem, menu→menuitem, clickable→button, links stay links; aria-disabled on disabled actionable items | Enter/Space on clickable items |
| QExpansionItem | Header role="button" with aria-expanded/aria-controls and localized expand/collapse label; collapsed content truly hidden | Enter/Space toggles |
| QTable | Native <table>; sortable headers with aria-sort and keyboard sorting; localized selection, pagination and loading names | Enter/Space sorts; standard controls elsewhere |
| QMarkupTable | Native <table> wrapper (scrollable region is a Tab stop) — header/scope/caption semantics are yours to author | Native |
| QTree | role="tree"/"treeitem"/"group" with aria-expanded/selected/checked; virtual mode adds aria-level/setsize/posinset | Roving tabindex over every visible node (disabled ones included, inert); arrows navigate/expand/collapse, Home/End, Enter selects, Space expands — or ticks, on a tickable node |
| QVirtualScroll | Padding hidden from AT; focus kept off <body> on slice changes; off-screen items don’t exist for AT | Scrollable container is a Tab stop when it owns scrolling |
| QTimeline | Native <ul>/<li> structure | Static content |
| QChatMessage | Text content readable; sent/received is visual-only — convey authorship via name | Static content |
| QCarousel | Navigation as a tablist with roving tabindex and per-slide labels; slides as tabpanels; localized arrows | Arrows (orientation- and RTL-aware), Home/End; selection follows focus |
Feedback and media
| Component | Built-in behavior | Keyboard |
|---|---|---|
| QBadge | role="status" (polite live region), aria-label from label | — |
| QBanner | role="alert" — announces when inserted dynamically | — |
| QChip | Clickable chips are role="button" (aria-pressed on selection chips); remove icon is keyboard-operable with a localized label | Enter/Space activates/removes |
| QLinearProgress / QCircularProgress | role="progressbar" with value ARIA (dropped while indeterminate) — add an aria-label | — |
| QAjaxBar | Progressbar ARIA while active, aria-hidden when idle | — |
| QSpinner | None — pair with a live region or hide as decorative | — |
| QInnerLoading | Visual overlay only — announce the state and manage the covered content yourself | — |
| QSkeleton | Decorative placeholder — mark loading regions yourself | — |
| QIcon | aria-hidden="true" always; override via attrs for semantic icons | — |
| QImg | role="img" named by alt; alt="" marks it decorative | — |
| QAvatar / QCard | Presentational containers; tag prop for semantic structure | — |
| QSeparator | Native <hr> with aria-orientation | — |
| QVideo | <iframe> named by the title prop — always provide it | Embedded player’s own |
| QParallax | Scroll-driven motion; use the media slot for meaningful imagery (alt) and consider reduced-motion users | — |
Layout and scrolling
| Component | Built-in behavior | Keyboard |
|---|---|---|
| QLayout | Landmark skeleton via children: <header>, <footer>, <aside>, <main> | — |
| QHeader / QFooter | Real <header>/<footer> landmarks; hidden marginals leave the AT tree; reveal-hidden ones re-reveal on focus | Focus re-reveals |
| QDrawer | <aside> landmark; backdrop and opener strip hidden from AT; no focus trap in overlay mode | Esc closes modal-state drawers |
| QPage | Renders the page’s <main> — don’t add another | — |
| QPageScroller | Positioning wrapper — put a real button in the slot for keyboard access | Via slotted button |
| QScrollArea | Custom scrollbars hidden from AT; the container is a Tab stop whenever content overflows | Native scroll keys once focusable |
| QSplitter | Full WAI-ARIA window splitter: role="separator", aria-controls, value ARIA, localized name | Arrows resize (RTL-aware), Home/End, Enter collapses/restores |
| QSlideItem | Swipe actions are pointer-only — provide a keyboard alternative | None built in |
| QInfiniteScroll | Loads on native scroll; loading state not announced | Works with keyboard scrolling |
| QPullToRefresh | Pointer gesture; expose trigger() through a button for keyboard users | Via your own button |
Purely visual or renderless utilities with no accessibility surface: QSpace, QPageSticky, QIntersection, QResizeObserver, QScrollObserver, QNoSsr, QSlideTransition and QResponsive.
External resources
- WCAG 2.2 — the Web Content Accessibility Guidelines quick reference
- WAI-ARIA Authoring Practices Guide (APG) — the interaction patterns Quasar components follow
- MDN Accessibility — practical HTML/ARIA reference
- WebAIM — articles, the contrast checker and screen reader survey data
- axe DevTools and Lighthouse — automated auditing in the browser and CI
- NVDA (Windows, free) and VoiceOver (built into macOS/iOS) — screen readers to test with