Important
Several behavior changes worth checking before you upgrade:
- QMenu and QTooltip lost the
scroll-targetprop. Popups now hear every scrolling container, nested ones included, through a single capture-phase document listener, so no container needs to be designated (and thescrollhelper class is no longer part of the contract). The prop was only ever an escape hatch for containers the auto-detection missed; remove it from your templates. - QMenu/QTooltip freeze their placement decision per show. A popup no longer re-flips or pins itself on screen while you scroll: it stays glued to its anchor and scrolls off-screen together with it, like a natively anchor-positioned element.
updatePosition()now means "re-run the placement decision"; a plain anchor move needs no call at all. - QTooltip tracks its anchor on container scroll instead of hiding.
fit/coverminimum sizing follows the real anchor box and is no longer inflated by theoffsetprop.- The
Platform.isflagsie,edgeChromium,winphone,kindleandsilkare gone (the first two were never set on any browser Quasar supports, the rest were dead platforms).is.edgenow plainly means Chromium Edge. - ESC dismissal and modal focus containment are no longer desktop-only. A mobile device with a hardware keyboard (iPad with a Magic Keyboard, Android with a BT keyboard, DeX) now closes dialogs, menus, drawers and tooltips with ESC and keeps Tab inside a modal dialog.
- Focus and hover feedback are keyed on input capability instead of the user agent. Focus paint moved to
:focus-visible, so a desktop mouse click no longer leaves a lingering halo, and hover paint moved under@media (any-hover: hover), so hybrids gain hover feedback without phones gaining sticky hover. - Build targets moved up: browsers to the live Baseline Widely Available minima (Chrome/Edge 121, Firefox 123, Safari/iOS 17.2) and Node to 22.
Perf
- perf(ui): zero idle cost for offscreen spinners -> spinners kept animating outside the viewport, burning main-thread style recalcs (a page of below-fold lazy QImg placeholders never went idle). Every spinner type now costs 0 recalcs and 0 layouts per second while offscreen, down from up to 173ms/s of style time, and visible spinners got 1.3-2x cheaper too. The SMIL paint animations were rewritten as CSS keyframes, preserving each spinner's timing, easing and phase relationships (#17151)
- perf(QCircularProgress): zero offscreen cost and a ~2x cheaper visible indeterminate mode -> 100 offscreen instances used to burn 84ms/s of style time; the onscreen cost drops from 42-66ms/s to 25ms/s per 100. QLinearProgress and QSkeleton were measured too and need nothing (#17151)
New
- feat(QMenu/QTooltip): native CSS anchor positioning on Chromium -> the anchor element gets a refcounted anchor name and the popup expresses its placement through
position-anchor/anchor()insets, so the browser keeps it glued through any scroll, layout shift, resize or animation with zero listeners. Every other engine keeps the JS positioning engine, whose behavior was aligned with the native one so a flip in Chromium and a flip in Safari are the same flip by construction. Menus now follow ANY scrolling container on both paths, ending the long-standing "the menu only follows a container carrying thescrollclass" limitation (the #8343 family) - feat(QSelect): open the options on hover -> new
hover,hover-delayandhover-hide-delayprops, the same contract QMenu, QBtnDropdown and QFab settled on. A hover-shown open never steals focus and never pretends to be focused, so a pointer merely passing over the select emits no focus/blur and cannot trip lazy validation;@filterselects still load their options on hover. A real click or Tab upgrades the show to a regular focused open (#11675) - feat(QSelect): close button for the options dialog ->
behavior="dialog"finally has a visible dismiss affordance, a labeled button in the in-dialog control row where the dropdown arrow would sit, so the software keyboard can never cover it. Follows thecolorprop, styleable throughq-select__dialog-close, opt out with the newhide-dialog-closeprop (#6858) - feat(QSelect):
no-chip-removeprop -> withuse-chips, drops the chips' remove icon and disarms the chip-armed Backspace removal, so a select that must always hold a value is possible with chips.clearable's own affordances stay intact when explicitly enabled alongside it (#17937) - feat(QSelect):
no-option-labelprop -> a plain no-options message without having to reach for theno-optionslot (#9463) - feat(QSelect): stable CSS class on the displayed value -> the selected-value span carries a dedicated
q-select__selected-valueclass as an officially targetable hook, e.g.white-space: pre-lineto render embedded newlines (#17973) - feat(QInput): support the v-model
lazymodifier -> Vue implements it only on native elements and leaves the deferral to components, which QInput ignored. The emission now stays pending until the change event or blur, matching the modifier's native semantics; an external model change cancels it (#18023) - feat(Dialog plugin): report the dismissal reason to
onCancel/onDismiss-> the callbacks receive'cancel','backdrop','escape'or'programmatic'(which includes an app route change), so the Cancel button can finally be told apart from a backdrop click, ESC or ahide()call. Custom components built onuseDialogPluginComponentget it with no template changes. New exported TS type:DialogDismissReason(#17938) - feat(BottomSheet plugin): report the dismissal reason to
onCancel/onDismiss-> the same argument,'backdrop','escape'or'programmatic' - feat(QParallax): react to scrolling in any container -> a parallax nested inside a second scrolling container silently stopped updating, since it only listened to the one auto-detected container. It now hears every scrolling container. The
scroll-targetprop stays with its other, narrower meaning: the box the scroll percentage is computed against - feat(ui): build for the current Baseline Widely Available browsers and Node 22 -> browser targets move off Vite's hard-coded snapshot to the live Baseline minima (Chrome/Edge 121, Firefox 123, Safari/iOS 17.2), to be refreshed on release trains, and both the Node build target and the long-stale
enginesfield (node >= 10!) move to Node 22
Fixes
- fix(QDialog): clip the viewport instead of pinning the body -> the scroll lock no longer moves the page to scroll 0 while a dialog, menu or drawer is open on non-iOS platforms, so nothing jumps on open or after dismissal (#18527, #18183)
- fix(QDialog,QMenu,QTooltip): finish a pending transition on keep-alive deactivation -> a popup deactivated mid-transition inside a
<KeepAlive>never received its transition-end, so it came back stuck half-faded (#18201, #18526) - fix(QDrawer): keep the drawer content alive across breakpoint crossings -> the close-swipe directive was attached only below the breakpoint and its condition is baked into the vnode key, so every mobile/desktop transition re-created the whole drawer content, and a containerized QLayout mounted it twice on every page load (#17099)
- fix(QIntersection): don't remount the content right after SSR hydration -> with
once+ssr-prerender, freshly hydrated content was destroyed and rebuilt on every page load (#17099) - fix(QTable): key slot content that can shift or swap so it never cross-patches -> cells rendered through
body-cell-<name>/header-cell-<name>slots were dropped unkeyed into the row, so changingvisible-columnsmade Vue diff one column's compiled slot output against another's, producing hybrid cells (one column's label with another's href) and worse after repeated toggles. Production-only, which is why it survived dev testing. Same treatment fortop-row/bottom-rowandtop-selection(#16047) - fix(QTable): hover sort icon previews the descending-first sort order -> hovering an unsorted column configured with
sortOrder: 'da'showed an up arrow even though the first click sorts descending (#18009) - fix(QVirtualScroll): shield the table padding cells from user sticky-column CSS -> the sticky-column recipe's
td:first-child { position: sticky }also matched the hidden spacer cells, and mutating a sticky cell during a scrollbar thumb drag made Chromium ratchet the scroller to the end (#17107) - fix(useVirtualScroll): reposition with an instant
scrollTo()-> a scroller with CSSscroll-behavior: smoothanimated the internal repositioning (#18168) - fix(QLayout): put
hide-scrollbaron the root element -> since Chrome/Edge 121 honorscrollbar-width, and it only reaches the viewport scrollbar from the root element, the anti-flicker resize logic had quietly stopped working (#17122) - fix(QImg): recheck an SVG's natural size after layout settles on WebKit -> Safari derives an SVG's
naturalWidth/naturalHeightfrom the img's current laid-out box, so when the load event lost the race with layout the wrong ratio got locked in andobject-fit: covercropped the image (#15652) - fix(QInput): keep the masked display while a debounced emission is pending -> with
unmasked-value, the first re-render after a keystroke rewrote the field to the raw text and it never recovered; the same happened after an IME composition - fix(QEditor): ignore holes in toolbar group definitions -> a stray comma in the
toolbarprop rendered fine but crashed on entering source view (#16940) - fix(QEditor): render toolbar tooltips on every platform -> they were gated behind a desktop user agent, leaving an iPad with a trackpad or mouse without tooltips and without the CTRL shortcut hints that matter exactly when a hardware keyboard is attached. Touch users additionally get the platform-standard press-and-hold tooltip
- fix(QForm): report a child whose
validate()returns nullish instead of breaking (#17921) - fix(QDialog): forward the ESC keyup event through
hide()-> thehide/before-hideevents carried no payload on ESC dismissal, so listeners could not tell a keyboard dismissal from a backdrop one - fix(QSelect): let hardware keyboards navigate options on mobile too -> arrow keys, Home/End, PageUp/PageDown and typeahead could never focus an option on a non-desktop UA, and each arrow press still scrolled back to option 0.
aria-activedescendantwas never set there either, breaking the combobox pattern's active-descendant navigation (#16599) - fix(QSelect): keyboard accessibility for the options dialog close button -> the button was outside WebKit's Tab loop, its focus indicator was hidden, and dismissing the dialog dropped focus on
bodyinstead of returning it to the control (#6858) - fix(QSelect): raise the iOS keyboard when the options dialog opens -> the dialog input ended up DOM-focused but keyboardless, because nothing was focused inside the tap's user activation window (#16196)
- fix(QSelect): open the options dialog when typing on the closed control -> with
behavior="dialog"anduse-input, typing into the focused but closed control was a dead end: the text piled up while the filter early-returned (#15976) - fix(QSelect): prefetch options to map a lazy loaded model value -> with
@filteroptions andmap-options, a preloaded model value displayed raw until a user interaction loaded the options. QSelect now fires that one filter call itself (#17983) - fix(QSelect): emit the triggering event with
@popup-show/@popup-hide-> both events always receivedundefined(#14678) - fix(QSelect): shield disabled options from Backspace removal -> a disabled option's chip showed no remove icon yet still died to Backspace (#15645)
- fix(QSelect): reset the typed input when focus leaves through the popup content -> clicking non-focusable popup content (such as the
no-optionslot) left the typed filter text behind in the input (#16135) - fix(QSelect): keep the page scroll position when the options dialog opens -> a leftover from the pinned-body era scrolled the page to top on dialog open, and it stayed there after dismissal
- fix(QSelect): drive the option hover highlight per pointer, not per UA -> hybrids the UA calls mobile (iPad with a trackpad, hovering pens) never got the highlight; a touch contact's own moves are still ignored so taps and scroll-drags cannot drag it around
- fix(QPopupProxy): declare the QMenu/QDialog pass-through props in its API -> all 31 forwarded props were missing from the API JSON, so the generated TS types rejected documented usage (e.g.
coverwithtransition-show) for JSX/TSX consumers and the docs API card listed none of them.separate-close-popupis the one documented exception: the menu branch overrides it (#18530) - fix(ui): arm ESC dismissal and focus containment on mobile platforms too -> see the note at the top (WCAG 1.4.13)
- fix(ui): key focus and hover feedback on input capability instead of UA -> see the note at the top. Covers QCheckbox, QRadio, QToggle, QSlider, QTable row hover, QChip and the
q-focusable/q-hoverablehelpers - fix(ui): paint the manual focused state on mobile platforms -> the whole focus-helper visual sat under
body.desktop, so the keyboard navigation QSelect just gained on mobile moved a focused state nothing painted (#16599)
Other
- API JSON now declares every pass-through surface it had been hiding: QPopupEdit gains the 15 QMenu props that already forwarded at runtime (target, context-menu, dark, the hover trio, dismissal and focus flags, transitions) plus the chained
escape-keyevent, and QVirtualScroll's entirely undocumented QList / QMarkupTable attrs forwarding is now declared. Both reach the generated TS types and the docs API cards - docs(api): the
errorprop reserves the bottom space even whenfalse-> thefalsevsnulldistinction (reserve the error line vs no external validation at all) was undocumented and kept getting reported as a bug (#17662) - A large internal cleanup pass: browser compat code obsolete under the new Baseline floor was removed (vendor-prefixed Fullscreen and Page Visibility, the passive-support probe, the RTL scrollLeft bug probe, QResizeObserver's
<object>fallback, IE-era normalize rules), the hover engine shared by QMenu/QFab/QSelect moved into one composable, and the API builder gained mixin/extends features that let components pull pass-through definitions straight from their source component instead of hand-copying them
Donations
Quasar Framework is an open-source MIT-licensed project made possible due to the generous contributions by sponsors and backers. If Quasar is useful in your workflow and you want to support ongoing maintenance, please consider the following: