Skip to page content

Accessibility
v2.25

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-modal doesn’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-visible polyfill 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

ComponentBuilt-in behaviorKeyboard
QBtnNative <button> or <a> (role="button" derived where needed); aria-disabled; progressbar ARIA while loading with percentageEnter/Space, including Space on link buttons
QBtnDropdownDisclosure pattern: aria-expanded, aria-controls (while the popup exists), localized expand/collapse label, opt-in toggle-aria-haspopupInherited from QBtn + QMenu
QBtnGroupVisual grouping only — add role="group" + aria-label yourselfEach button is its own Tab stop
QBtnTogglearia-pressed per option; per-option attrs for labelsEnter/Space per button
QFabaria-expanded/aria-controls on the trigger; closed actions hidden from AT; focus returns to triggerEnter/Space opens and activates
ComponentBuilt-in behaviorKeyboard
QTabsrole="tablist"/"tab" with aria-selected, aria-orientationRoving tabindex; arrows (RTL-aware), Home/End; explicit activation
QTabPanelsrole="tabpanel", tabindex="0"; tab↔panel id wiring is manual (documented)Panel reachable via Tab
QBreadcrumbsNative links; no landmark/aria-current — wrap in <nav> and mark the current page yourselfNative link behavior
QPaginationNamed role="navigation"; localized first/prev/next/last labels; aria-current on the active pageEnter/Space on buttons; Enter commits input mode
QStepperaria-current="step"; each step a labeled group; navigable headers exposed as buttons (disabled ones as aria-disabled buttons)Enter/Space on navigable headers
QToolbar / QBarrole="toolbar" — name it via aria-label when a page has severalChildren are individual Tab stops

Form fields

ComponentBuilt-in behaviorKeyboard
QField / QInput<label for> wiring; errors announced via role="alert" with aria-invalid/aria-errormessage/aria-describedby (only while the message renders); accessible clear buttonNative editing; Enter/Space on the clear button
QSelectFull combobox pattern: role="combobox" + aria-expanded/aria-controls/aria-activedescendant, role="listbox"/"option" with aria-selected and virtual-scroll-aware aria-setsize/posinsetArrows open/navigate, typeahead, Home/End, PageUp/PageDown, Enter selects, Esc closes
QFormNative <form>; focuses the first invalid field on failed validationNative submit
QEditorrole="textbox" + aria-multiline; toolbar follows the APG toolbar pattern with localized labelsToolbar roving tabindex, arrows/Home/End; Ctrl formatting shortcuts
QFileField frame + validation ARIA; keyboard-openable pickerEnter/Space opens the picker
QUploaderReal buttons with localized names, progressbar ARIA — but you must still announce status changesNative button activation

Form controls

ComponentBuilt-in behaviorKeyboard
QCheckboxrole="checkbox", three-state aria-checked (incl. mixed), aria-label from labelEnter/Space toggles
QRadiorole="radio" + aria-checked; group semantics come from QOptionGroupEnter/Space selects
QTogglerole="switch" + aria-checkedEnter/Space toggles
QOptionGrouprole="radiogroup"/"group"; APG radio-group patternRoving tabindex; arrows select (RTL-aware), skipping disabled options
QSlider / QRangerole="slider" with aria-valuemin/max/now, aria-orientation, aria-readonly (QRange: one named slider per thumb)Arrows step (RTL/vertical-aware), PageUp/PageDown ×10
QRatingRadio-group pattern per star with per-star labels (icon-aria-label)Roving tabindex; arrows move, Enter/Space select
QKnobrole="slider" with value ARIA on the focusable elementArrows step, PageUp/PageDown ×10
QColorTune tab (native inputs + sliders) is the accessible path, with localized tab/field/slider names; palette/spectrum are pointer-onlyNative inputs + slider keys on the Tune tab
QDateDay buttons with full-date labels, aria-pressed selection, aria-current="date" for today; localized navigationRoving tabindex; arrows cross months, Home/End, PageUp/PageDown (+Shift for year)
QTimeHeader role="spinbutton" per unit with localized labels and value ARIA; clock face is a pointer-only visualizationArrows step, Home/End, direct digit entry

Popups

ComponentBuilt-in behaviorKeyboard
QDialogrole="dialog" + aria-modal; focus moved in on open, contained while modal, restored on close — name it via aria-label/aria-labelledbyEsc dismisses (persistent dialogs shake instead)
QMenuRole-less popup by design; declare role="menu" on the QList inside; aria-expanded on a control anchor; focus restored on closeEsc closes; Tab past the edge closes and continues from the anchor
QTooltipDescribes its anchor via aria-describedby while shown — never names it; role="tooltip"; shows on keyboard focusEsc dismisses without moving focus (WCAG 1.4.13)
QPopupProxySwitches between QMenu and QDialog by screen size — semantics follow the rendered componentDelegated
QPopupEditQMenu-based edit surface; localized Set/Cancel buttons; never commits silently on dismissalEsc cancels; wire Enter-to-save yourself

Lists and data

ComponentBuilt-in behaviorKeyboard
QList / QItemContext-derived roles: list/listitem, menumenuitem, clickable→button, links stay links; aria-disabled on disabled actionable itemsEnter/Space on clickable items
QExpansionItemHeader role="button" with aria-expanded/aria-controls and localized expand/collapse label; collapsed content truly hiddenEnter/Space toggles
QTableNative <table>; sortable headers with aria-sort and keyboard sorting; localized selection, pagination and loading namesEnter/Space sorts; standard controls elsewhere
QMarkupTableNative <table> wrapper (scrollable region is a Tab stop) — header/scope/caption semantics are yours to authorNative
QTreerole="tree"/"treeitem"/"group" with aria-expanded/selected/checked; virtual mode adds aria-level/setsize/posinsetRoving 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
QVirtualScrollPadding hidden from AT; focus kept off <body> on slice changes; off-screen items don’t exist for ATScrollable container is a Tab stop when it owns scrolling
QTimelineNative <ul>/<li> structureStatic content
QChatMessageText content readable; sent/received is visual-only — convey authorship via nameStatic content
QCarouselNavigation as a tablist with roving tabindex and per-slide labels; slides as tabpanels; localized arrowsArrows (orientation- and RTL-aware), Home/End; selection follows focus

Feedback and media

ComponentBuilt-in behaviorKeyboard
QBadgerole="status" (polite live region), aria-label from label
QBannerrole="alert" — announces when inserted dynamically
QChipClickable chips are role="button" (aria-pressed on selection chips); remove icon is keyboard-operable with a localized labelEnter/Space activates/removes
QLinearProgress / QCircularProgressrole="progressbar" with value ARIA (dropped while indeterminate) — add an aria-label
QAjaxBarProgressbar ARIA while active, aria-hidden when idle
QSpinnerNone — pair with a live region or hide as decorative
QInnerLoadingVisual overlay only — announce the state and manage the covered content yourself
QSkeletonDecorative placeholder — mark loading regions yourself
QIconaria-hidden="true" always; override via attrs for semantic icons
QImgrole="img" named by alt; alt="" marks it decorative
QAvatar / QCardPresentational containers; tag prop for semantic structure
QSeparatorNative <hr> with aria-orientation
QVideo<iframe> named by the title prop — always provide itEmbedded player’s own
QParallaxScroll-driven motion; use the media slot for meaningful imagery (alt) and consider reduced-motion users

Layout and scrolling

ComponentBuilt-in behaviorKeyboard
QLayoutLandmark skeleton via children: <header>, <footer>, <aside>, <main>
QHeader / QFooterReal <header>/<footer> landmarks; hidden marginals leave the AT tree; reveal-hidden ones re-reveal on focusFocus re-reveals
QDrawer<aside> landmark; backdrop and opener strip hidden from AT; no focus trap in overlay modeEsc closes modal-state drawers
QPageRenders the page’s <main> — don’t add another
QPageScrollerPositioning wrapper — put a real button in the slot for keyboard accessVia slotted button
QScrollAreaCustom scrollbars hidden from AT; the container is a Tab stop whenever content overflowsNative scroll keys once focusable
QSplitterFull WAI-ARIA window splitter: role="separator", aria-controls, value ARIA, localized nameArrows resize (RTL-aware), Home/End, Enter collapses/restores
QSlideItemSwipe actions are pointer-only — provide a keyboard alternativeNone built in
QInfiniteScrollLoads on native scroll; loading state not announcedWorks with keyboard scrolling
QPullToRefreshPointer gesture; expose trigger() through a button for keyboard usersVia 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