NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Enterprise-ready, AI-ready cross-platform Vue.js framework: high-performance, accessible components for SPA, PWA, SSR, SSG, mobile and desktop apps, with documentation your AI coding agent reads offline
Last release 3 days ago
07 Oct 2026
Release timing varies
gaps range from 8 days to 2 months
Some releases are documented
notes for 18 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
11 years old
448 releases · first in 2015
feat(colors): rgbToText() -> new util on the colors object, the inverse of textToRgb() : turns an { r, g, b, a } object into an rgb() / rgba() string,
colors object, the inverse of textToRgb(): turns an { r, g, b, a } object into an rgb()/rgba() string, just as rgbToHex() mirrors hexToRgb()#ff000001 no longer turns it into #ff000000 (which also left the header out of sync), and #..02 no longer becomes #..03. An rgba() alpha with more than two decimals is kept instead of rounded. The Tune alpha field now accepts and shows a fractional percent (the sliders keep whole-percent steps). The color utils change along with it: hexToRgb() now returns the alpha as a percent with one decimal (#..80 gives a: 50.2, not 50), and textToRgb() keeps the decimals of an rgba() alpha, so a color comes back unchanged after a round trip through the utils (#18562)$q.screen.orientation stays at its 'portrait-primary' default, the same as on the server (#18561)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:
One column per quarter.
feat(ui): useElementSize() composable -> reactive elementSize ( { width, height } ) of an element or component ref, with an onResize hook, a debounce
elementSize ({ width, height }) of an element or component ref, with an onResize hook, a debounce window and a disabled switch; QResizeObserver is now built on itv-resize:100), once modifier, a handler returning false stops itonScroll hook, axis, debounce and disabled; QScrollObserver is now built on itopenFilePicker() resolves with the accepted files (null when dismissed) and the files get validated by the same rules as QFile (accept, maxFileSize, maxTotalSize, maxFiles, filter...)isOverDropZone (the directive sets a q-drop-zone--over class instead) and rejected-file reporting. QFile and QUploader now handle drops through itmutationRecords as reactive statefullscreen prop of QTable, QCarousel and QEditor (back button, route-change exit, scroll restore, popups kept on top), now usable on any element or componentisIdle flips after timeout ms with no user activity, plus lastActive and resetIdle(); hidden-page time counts as inactivity?worker import): status, last message and error, postWorkerMessage(), a terminateWorker() that frees the thread until the next message, and onCreate/onMessage/onError/onTerminate(reason) hooksrunWorkerFn(...args) resolves with its result, with timeout, Transferables, script dependencies, one hook per outcome (onSuccess/onError/onTimeout/onTerminate) and an inline run on the SSR serverfn.isPending is reactive on the useDebounce() one{ leading, trailing, maxWait } as its third param (a boolean still means "immediate") and gains flush() plus an isPending property; throttle() gains cancel(), flush() and an opt-in trailing call (throttle(fn, limit, true)) so a burst no longer ends on a stale stateisTimeoutPending / isIntervalActive reactive flagsrefreshIntersection() and stopIntersection(), named after the composable like the new ones; refresh()/stop() keep workingdisabled, onError, typed keys through the augmentable LocalStorageItems/SessionStorageItems interfaces, SSR-safe$q.network with the online state and, where the browser has the Network Information API, effectiveType/downlink/rtt/saveData$q.wakeLock wraps the Screen Wake Lock API (isCapable, isActive, request/release/toggle) and re-acquires the lock when the page becomes visible again$q.screen.orientation -> reactive { type, angle, portrait, landscape } of the device screenvertical prop -> stacks the buttons in a column, with the corners and seams drawn for that direction (#7766)align prop -> aligns the content of every option button (overridable per option), so the icons of a vertical toggle line uphorizontal prop -> loads more when the row is scrolled to its inline end (the left one in RTL), reverse included (#6709)body-expand slot -> an expansion row declared after each body row while QTable keeps rendering the row itself (and its body-cell-[name] slots); no more taking over the whole row through the body slot (#4380)expandable prop -> an expansion row that can slide open and closed with a QSlideTransition inside its cell (#4380)field is optional -> a column rendered entirely through its body-cell-[name] slot needs nonetarget that resolves after the model turns on (v-model written before :target) -> the popup is anchored to it instead of rendering at its static position in Chromium (#18559)once observation that fired resumes observing when once goes awaynull value disables it instead of throwing; the v-intersection/v-mutation/v-resize/v-drop-zone disable values are typed as false | null instead of Boolean, which invited trueaxis="both" -> no longer emits an unchanged position (on mount, on an RTL flip); getPosition() returns a snapshot instead of the live internal objectsetDebounce() -> the pending update of the previous listener is applied right away instead of running late on a target it was removed fromsetItem(key, null | undefined) removes the item, so a null read always means a missing item (older versions stored the strings the browser made of them); a stored RegExp keeps its flags (what older versions stored still decodes); an item stored natively as an empty String reads as '' instead of nullgetAll() decodes and parses the values as get() does; on the server, set() replaces the request cookie so getAll() no longer returns the previous value; remove() is typed to accept the same attributes as set() (a __Secure-/__Host- cookie needs secure) and set() the Object/Array/Number values it always storeduseTemplateRef('btn').value.click(), .validate()...)MetaOptions and noop are importable from "quasar"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:
fix(QField): custom standout text color -> a text-* class in a string standout (e.g. standout="bg-teal text-yellow" ) now colors the input, prefix, su
text-* class in a string standout (e.g. standout="bg-teal text-yellow") now colors the input, prefix, suffix, prepend and append too, not just the label. Before, the focused standout painted those in hard white (black in dark mode). A string without a text color keeps the theme default for the content, and its label now uses that default too instead of the primary color (#16019)https:// shown as a placeholder instead of pre-filled as a value. Pasting a copied URL no longer produces a broken https://https://... link (#11450)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:
fix(QPagination): the page input clears once the page is submitted -> in input mode the typed text stayed in the field after pressing Enter, so on a 1
input mode the typed text stayed in the field after pressing Enter, so on a 10-page pagination the field read "99" next to a placeholder reading "10 / 10". The field now empties and shows the current page again (#18552)textToRgb() keeps the alpha of the rgb(R, G, B, A) form; only rgba() preserved it, rgb(255, 0, 0, 0.5) lost its fourth argument. rgba() written without an alpha still yields a: 100 (#18555)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:
feat(ui): AI-ready out of the box -> your AI coding agent now works from the Quasar documentation of the exact version installed in your project: ever
@quasar/mcp server and your agent looks things up instead of guessingextractDate() matches the longest locale name instead of the first one declared, so what formatDate() writes parses back to the same date. Previously a name that prefixes a longer one won: vi "2023 Th12" under YYYY MMM parsed as January, tr "Pazar" swallowed "Pazartesi". 16 such round-trips across 7 of the shipped language packs are fixed (#18548)ddd parsed back as Mondayv-model:pagination it kept offering the page size the table was mounted with and omitted the one in use (#18550)scrollTo() -> the to of the emitted virtual-scroll payload is the index of the last rendered row, as documented. It was stale under a controlled pagination, -1 with "all rows" and past the end on a partially filled last pagescrollTo() called right after a scroll event is no longer dropped when the scroller is still moving (momentum, a clamp after a resize) on a device with slow framesQuasar 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:
chore(css): Baseline cleanup -> appearance: none is emitted unprefixed (the -webkit- form served no supported browser), and the fieldset padding rule
appearance: none is emitted unprefixed (the -webkit- form served no supported browser), and the fieldset padding rule left normalize since every supported engine ships the same values in its UA stylesheet. No rendering change on Chromium 121+, Firefox 123+ and Safari 17.2+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:
fix(openURL): DOM clobbering of the iOS SafariViewController bridge -> on iOS, attacker-controlled HTML rendered into the page (a <a id="SafariViewCon
<a id="SafariViewController"> through QEditor, or QSelect / QChatMessage with HTML rendering enabled) shadowed the native bridge global, so every later openURL() call threw TypeError: window.SafariViewController.isAvailable is not a function and the URL never opened. openURL() now hands the URL to the bridge only when it exposes the expected isAvailable() and show() functions, and opens a regular window otherwise (GHSA-89vp-x45c-52cq)framework.config.globalNodes -> the option has been honored since #8634 (it sets the CSS class of the nodes Quasar appends to <body> for portals, Notify, Loading and LoadingBar) but was never declared in QuasarUIConfiguration, so a typed quasar.config file or app.use(Quasar, { config: { globalNodes } }) rejected it. It is typed now, with its class member documentedQuasar 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:
fix(QExpansionItem): group with several items opened on mount -> two or more items of the same group that both start with model-value: true (or defaul
group that both start with model-value: true (or default-opened) threw on mount since v2.32.0 and were left with every body visible under a collapsed header. The group sibling that has to close is now settled closed before its content element is ready, with after-hide emitted as usual (#18547)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:
Important Behavior changes worth checking before you upgrade:
Important
Behavior changes worth checking before you upgrade:
<img> into the SSR/SSG HTML whenever the box shape is known (ratio, initial-ratio or height set), instead of waiting for hydration. The markup a crawler or a no-JS client sees changes accordingly. Images with none of the three keep deferring to the client, unless you opt in with the new ssr-prerender prop..q-responsive__filler element no longer exists, and neither does QImg's filler node: the ratio now sits on the component root through CSS aspect-ratio. A QVideo with an explicit width resolves its ratio against that width instead of the parent's, and a QResponsive inside a height-clamped parent keeps its ratio by narrowing rather than squishing its content (Chromium and Firefox; WebKit keeps the previous full-width box).duration prop is now a real Number prop on the component itself rather than a passthrough to the child.height/transition styles while sliding: the slide runs as a Web Animation over a calc-size(auto, size) keyframe. Only overflow-y is written during the slide. Firefox and Safari keep the previous measuring CSS transition unchanged.field-sizing: content where it is supported (Chromium 123+, Safari 26+), so no inline height/overflow styles appear on the textarea any more and a max-height scrolls natively. Older engines keep the JS measuring fallback.scroll event and the content slot scope still work, but they are what forces the JS tracking back on, so use the slot only where you need its scope.ssr-prerender -> until now QImg only set the image source once the client had mounted, so under SSR/SSG the browser started fetching it after the JS bundle had downloaded and the app had hydrated: the preload scanner never saw it, crawlers and no-JS clients got an empty box, and any hero image behind QImg was a late LCP by construction. The server and the pre-hydration client now render the <img> themselves, visible from the first paint like a native image, with the placeholder underneath; on mount the component reconciles against the element it finds instead of trusting load events, so an image that finished before hydration still goes through the regular load (or error) path. Only images whose box shape is known (ratio, initial-ratio or height) are server-rendered by default, since otherwise the box would visibly snap from the default ratio to the natural one at hydration; the new ssr-prerender Boolean prop opts the rest inpan event with the stage of the swipe gestures -> the swipe-to-open and swipe-to-close gestures ran silently: an app could only learn that the drawer had opened or closed after the fact, through the model and the show/hide events, never that a swipe was in progress, so content mounted on open only came in once the drawer had already been dragged fully into view and the swipe itself showed an empty panel. QDrawer now emits pan with the gesture type ('open' or 'close') and its stage: 'start' once the pan is detected, 'end' when it crossed the threshold and show()/hide() ran, 'cancel' when the finger let go short of it and the drawer sprang backrefresh() method -> everything the component settles at mount (the scrolling container and the way the media is moved along it, the media size, the position) was only re-checked on the events it listens to, so an ancestor changing its overflow, a media swapped without a load event or a scroll-target that only appears later left it stale with no way to tell it so. refresh() re-runs all threeElement.animate() on a ViewTimeline of the component root wherever the API exists and nothing scrollable sits between the root and its scroll target. The browser then moves it on its own thread, in step with the scroll whatever the main thread is doing, where the JS tracking wrote its transform a frame behind (one scroll step off in every other compositor frame) and stalled together with the main thread; the keyframes carry the exact numbers the JS tracking used, so the movement itself is unchanged. On that path the component registers no scroll or resize listener and no IntersectionObserver at all, the tracking being created only where the scroll event or the content slot needs the percentage, and there it reads the timeline's own progress instead of measuring. Firefox, and any layout the timeline cannot express (an overflow: hidden wrapper, a scroll-target that is not the nearest scroll container), keep the JS tracking, itself now measuring once per frame instead of once per scroll event. The media is also no longer left hidden until the first measurement, and a media without a natural size yet no longer measures a display: none boxcalc-size() is supported -> the height slide (so QExpansionItem, QTree and the vertical QStep) measured the content on every toggle, one scrollHeight read on show and two on hide, each forcing a layout: toggling N nodes in one tick (QTree expand/collapse all, filtering) forced N layouts on show and 2N on hide. Where the engine sizes a calc-size(auto, size) keyframe at layout (Chromium 129+) the slide is now an Element.animate() between 0 and that keyframe: no measurement, no forced layout, the height tracked live while sliding, and an interrupted slide of a kept element played backwards. Engines without calc-size() keep the measuring CSS transition, untouched. Chromium 153 headless, 200 boxes toggled in one tick: forced layouts on show 219 -> 20 and on hide 418 -> 19, layout time 13.7ms -> 8.4ms and 22.2ms -> 6.3msv-show. They now call the slide engine on the element themselves. Chromium 153 headless, a QTree with 780 nodes (155 parents) mounted expanded, median of 12 mounts: 43.1ms -> 32.3msfield-sizing where supported -> autogrow textareas now get field-sizing: content and skip the JS measuring routine entirely on browsers that implement the property: no rAF layout thrash per keystroke, no inline height/overflow writes, no scrollTop juggling, and the browser handles a max-height with native scrolling. Browsers below the property's floor (Chromium before 123, Safari before 26) keep the routine, which also coalesces to one measurement per frame now (a keystroke used to schedule two) and cancels a pending frame when autogrow turns off or the component unmounts. Height parity between the two paths was verified in Chromium, Firefox and WebKit across plain, labeled, outlined, filled, dense, max-height and custom font-size casesaspect-ratio -> the padding-bottom percentage hack needed a dedicated filler element in QImg and QResponsive (plus a width-inheriting wrapper in the latter) and a zero-height root in QVideo. aspect-ratio is under the Baseline floor, so the ratio now sits on the root element itself and those nodes are gone: one element fewer per QImg, two fewer per QResponsive. See the note at the topQuasar 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:
Important Behavior changes worth checking before you upgrade:
Important
Behavior changes worth checking before you upgrade:
hide-hint hint), emits focus/blur and validates on blur with lazy-rules. Only disable now exempts a field from counting as focused, matching the existing rule that only disable exempts it from validation.config.dark: 'auto' resolves again on SSR/SSG apps, on the client once it takes over. It has silently been a plain light false since v2.18.2. The first paint stays light (the server cannot know the preference), so the documented cookie approach remains the way to a flash-free page.fa and fa-IR language packs render Persian digits in QDate, QTime and QPagination, through the new root-level formatNumber pack key..q-panel.scroll wrapper element; its classes moved onto .q-stepper__step-content, now a direct descendant of .q-stepper__step.q-transition--field-message-* classes no longer exist. Restyle a QField message swap through .q-field__messages--animated instead; the leave half of the animation (the old message fading out under the new one) is gone.palette slot, and the picker is fully keyboard-accessible -> the palette view had no way to be customized: swatches were plain divs, laid out 10 per row, reachable by pointer only. The new scoped palette slot replaces the default swatches while keeping the other views and the view switcher, handing over the resolved colors (palette), a select(color) function and an editable flag. The default swatches become buttons following the roving tabindex pattern (arrows move, Up/Down by one visual row, Home/End jump to the edges, Enter/Space picks), and the spectrum panel, the last pointer-only part of the picker, becomes a WAI-ARIA slider over its two axes: arrows step the saturation and the brightness by one, ten with Shift, Home/End and PageUp/PageDown jump. Both follow the visual direction in RTL, both draw a two-tone inset focus ring that stays visible on any color, and the spectrum spells out its value through two new lang-pack keys, colorPicker.saturation and colorPicker.brightness, translated across every pack (#8613)$shadow-color/$dark-shadow-color Sass variables, so the UMD build, the Vite plugin and every other prebuilt-CSS flavour had no way to revert or tune it short of restyling each component. Both colors are now declared on :root as --q-shadow-color and --q-dark-shadow-color, with the Sass variables as their defaults so existing Sass overrides keep working. Nothing moves by a pixel out of the box, and the pre-v2.11 look is a one-liner (body.body--dark { --q-dark-shadow-color: #000 }); transparent removes the shadows altogether. The tints are derived on body, so the tokens are read at the :root/body level (a body.body--dark rule, setCssVar()), not on a deeper element (#15144)formatNumber pack key, localized digits in QDate, QTime and QPagination -> a Persian calendar coupled with the fa-IR pack showed Persian month names next to Western day numbers, with no way to change that short of a font that remaps the ASCII digit glyphs. Language packs (and the locale prop) can now define an optional root-level formatNumber(value), which receives the ASCII digit string the component would display, zero-padded where it is padded ("5", "05", "1403"), and returns the localized rendering. QDate routes its day cells, navigation year, years grid, header title and subtitle, the "N days" count and the day buttons' aria-label through it, QTime its header units and clock face, QPagination its page buttons and their aria-label. Model strings, date hashes and QTime's stringModel stay ASCII. The fa and fa-IR packs opt in; every other pack is unchanged (#12735)autoWidth in the column definition -> shrinking a column to its content required a per-column header-cell and body-cell slot override with <q-th auto-width>/<q-td auto-width>, cumbersome for dynamically built columns, the only alternative being an undocumented class through classes and headerClasses. A column can now simply declare autoWidth: true, which applies to the auto-rendered cells as well as to QTh/QTd fed with the slot props (#8206)v-close-popup that opens a dialog made the two portals compete for focus, and the closing one won on both counts: the dialog opened without focus in its autofocus field (on Android the soft keyboard never showed), and it had captured, as the element to return focus to, an element inside the closing portal, destroyed with it, so closing the dialog dropped focus altogether. A portal closing while another one sits on top of it in its opening transition now hands its own anchor to the opener instead of restoring focus itself; portals that take no focus (no-focus, hover-shown menus) or that are already fully open decline the hand-off, so everything else is unchanged (#8211)readonly was still true got dropped and nothing re-synced afterwards: a handler flipping readonly off and calling focus() in the same tick left the input holding DOM focus with the field unfocused, label resting over the caret. The gate itself was the problem, not its timing, since a readonly control stays in the tab order and a keyboard user tabbing onto it got no visible focus indication at all. The focused state now mirrors DOM focus, only disable keeping a field from counting as focused. See the note at the top (#16056)control slot wrapper to the control -> with a control slot, autofocus, focus() and every other way of focusing the field (QForm/QDialog/QMenu autofocus, clearValue, a click around the control) landed on the q-field__native wrapper: the field looked focused and emitted focus, but the slotted control never received the caret. The wrapper now forwards any focus it receives to the element bound to the slot scope id, else to the first focusable element of the slot, and keeps it only when the slot holds nothing focusable (#12648)backface-visibility: hidden, which Chromium answers with a permanent compositor layer per label. With a few hundred fields on a page each hover repaint re-runs the layer assignment pass against all of those interleaved layers and it goes quadratic: on the reported layout (100 rows x 5 labelled number inputs) headless Chromium held ~22 fps with 50-67 ms frames while native inputs held 60. The rule dates from the Qv1 stylesheets with no recorded purpose and the settled label renders pixel-identical without it. Measured after: 6 composited layers instead of 461, ~65 ms of Layerize per hover sweep instead of ~1.7 s (#16024)new-value-mode/@new-value, Enter or Tab then re-selected that option and silently dropped the text the user had just typed, @new-value never being emitted. The highlight now remembers whether it was placed automatically or by the user, and only the automatic one yields to typed text under new-value mode: navigated or hovered options still win, fill-input's filled-in label is not user text so Enter right after opening still re-selects, and selects without new-value mode never consult it (#16514)$q.screen (:vertical="$q.screen.lt.sm") lost the active step's content, form state included, whenever the viewport crossed the breakpoint, and keep-alive could not bridge it either, the two orientations rendering different trees with two different caches. Both orientations now render every QStep under .q-stepper__content, with one plain Transition per step driving the vertical height slide and the horizontal slide alike. keep-alive-max, which the vertical mode never honoured, is now counted by the stepper itself, and the stepper's transition-duration also drives the vertical slides (default unchanged). See the note at the top (#9703)active-icon="none" -> the header decided the dot's content with two computeds that disagreed on which state wins, so an active step that also had error, with active-icon set to none, got neither the icon nor the prefix and the dot was left empty; a step-level active/error/done-icon of none hit the same blank. The state icon is now resolved once, with the step prop taking precedence over the stepper one, and both the prefix decision and the rendered icon derive from it (#15259)--q-drawer-width custom property, documented as usable by user CSS, and the mini rule sizes the item from it, so 24px icons land on the standard inset at any scale factor, bordered or not (#18320)position: fixed, which collapses the root element to zero height, and a body background propagates to the canvas with the root box as its positioning area: a gradient or a cover-sized image stopped painting the moment a popup locked the page, showing a blank canvas behind the dialog. A solid color needs no positioning area, which is why the default dark page never showed it, and desktop and Android use the viewport clip lock, so only iOS was affected. The lock class now gives the root element min-height: 100%, min-height so user html { height } rules still win (#16799)offsetWidth/offsetHeight, the border box, but observed the default content box, so a padding or border change on the observed element itself grew the border box without touching the content box and no notification fired. QHeader, QFooter and QDrawer size the layout through it, so a padding-top: env(safe-area-inset-top) rule placed on .q-header (the iOS PWA status-bar pattern) left QPageContainer with a page padding that ignored the inset until the device rotated. Padding on the toolbar child, which is what Quasar's own q-ios-padding/q-safe-area-padding rules use, was already tracked (#7526)q-mt-md card, say) collapsed through it and pushed the page down inside QPageContainer; together with the page's min-height the layout then exceeded the viewport and the window grew a scrollbar even when the content fit the screen. A last child's bottom margin did the same at the other end. The page now establishes a block formatting context, through a zero-specificity rule so display utilities on the page and user overrides keep winning (#9536)'auto' resolves on SSR/SSG clients once they take over -> since v2.18.2 the ssr-client build reads its initial dark state from the server-emitted body class, and the server renders 'auto' as light, so config.dark: 'auto' ended up as a plain false on every SSR/SSG app: no media query listener, light forever. The server now flags an 'auto' render with a body attribute, and the client resolves it on takeover unless the app already set an explicit value meanwhile. A SSR+PWA shell boot, which has no server markup to read, starts from the config like an SPA does; the config was silently dropped there too. See the note at the top (#7530)immediate option could re-enter itself from within its own invocation and fire again instead of being held back for the wait period (#18544)morph() defers its work to a requestAnimationFrame callback, and when the destination's subtree was removed in between (a v-if or an unmount in the same tick as the model change) the validity check still accepted it, because a detached subtree keeps its parentNode, and the fixed-position parent walk then reached null and threw inside getComputedStyleerror prop rendered a Transition component around its messages element, only to animate the hint-to-error swap. With Vue 3.5 that instance is about a quarter of a QInput's mount JS (2000 labelled inputs with hints: 148 ms to 113 ms on Chromium), and it was the single biggest item in the reporter's own devtools timeline. The messages element is keyed by its content, so a swap already re-creates it, and a keyframe on the new element reproduces the enter half of the old transition; nothing animates on page load and no field gets a composited layer at mount. See the note at the top (#12369)quasar dev compiles the Sass verbatim, so dev servers paid for a layer per radio, checkbox, toggle, slider ring, linear progress, date indicator, tree arrow, spinner and notification badge; the JS-built strings (QLinearProgress, QCircularProgress and through it QKnob, QAjaxBar, Ripple, QTabs' indicator) reached production as written, so those are a production win too. Every value is rewritten as its exact 2D equivalent and the minified quasar.prod.css/quasar.rtl.prod.css are byte-identical before and after. QLayout's layer hack and QParallax's media transform stay 3D on purposeQuasar 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:
fix(ui): every directive guards its context again in updated / beforeUnmount -> Vue defers a directive's updated to the post-flush queue but runs befo
updated/beforeUnmount -> Vue defers a directive's updated to the post-flush queue but runs beforeUnmount synchronously, so a queued updated can outlive the element's unmount and find its context already gone. v2.30.0 dropped the early return that covered this, and the hook threw a TypeError. In production Vue logs it; in development it rethrows, flushJobs never clears currentFlushPromise and nothing reactive is ever flushed again, freezing the app on whatever was painted. Reported for the four touch directives (a QScrollArea inside <keep-alive :include>, dropped from the cache in the same tick it re-renders), the guard is restored across all eleven: v-close-popup, v-intersection, v-morph, v-mutation, v-ripple, v-scroll, v-scroll-fire and v-touch-hold/v-touch-pan/v-touch-repeat/v-touch-swipe (#18543)@load and renders a loading placeholder taller than offset while that runs (from its own mounted hook, a nextTick or an async component) had already grown the content by then, so that report read "out of reach" and an observer never re-reports a state that merely holds: @load never fired and the placeholder waited for data nobody had asked for. The first load is decided on mount again, off the DOM as it stands, by the same reach test the observer applies, and it is no longer routed through the debounced poll, so a paused fake clock (Playwright's page.clock) does not hold it back either. A component mounted out of reach stays quiet as before, and the observer still owns every load after the first (#18542)v-ripple re-read its argument and modifiers only when the value reference changed and only for an object value, so three things were frozen at mount: v-ripple:[color] with no value never picked up a new color, a modifiers object that changed while the value did not was ignored (QBtn hits this on its own, so toggling its round prop left the ripple starting from the press point instead of the center, or the other way round), and setting the value back to true after an object kept that object's options. All three now re-derive (#18541)deepEqual() handles null-prototype objects -> comparing two objects created with Object.create(null) threw, because the valueOf fallback saw a missing method as an override of Object.prototype's and called it. Both conversion fallbacks now require an actual callable on both sides, so null-prototype objects, and objects whose own valueOf/toString is not a function, compare by their keys. Two objects of the same constructor where only one carries such an override are compared key by key too, instead of through that one side's conversion (#18540)header-<name> slot defaults styled .q-icon and .q-avatar inside the node header at a specificity that tied with each component's own icon sizing and won on source order, so a QBtn placed in a header slot rendered a fixed 21px icon whatever its size prop said. The defaults are scoped through :where() and lose every tie, while still applying to plain slot icons (#16753)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:
Important Behavior changes worth checking before you upgrade:
Important
Behavior changes worth checking before you upgrade:
offset is now measured from the end of the content instead of the end of the scroll target (identical whenever the component is the last thing in its scroller), nothing loads once you scrolled past the component, debounce counts from the moment the end comes within the offset rather than from the last scroll event, the mount-time load lands one frame later instead of synchronously, and a scroll-target outside the component's ancestry no longer clips it.v-scroll-fire fires as soon as any part of the element is visible, where it used to wait for the element's bottom edge to enter the container (pass the new :1 argument for the old timing; elements taller than the viewport never reach it). It also fires when the element becomes visible without a scroll, and the 25ms debounce is gone.v-intersection dispatches every entry of a callback batch (previously only the first per callback, which could swallow a fast in-then-out), no longer passes the observer as an undocumented second argument, and ignores entries queued by an element's former observer.extractDate() returns an Invalid Date for a week mask that cannot resolve (week 0, week 54, week 53 of a 52-week year, or no year at all) where it used to return a date in 1900; masks containing dd now match instead of never matching.v-touch-hold ignores a press while another one is tracked, so a two-finger hold fires the handler once instead of twice.cursor-position and QMenu's touch-position/context-menu re-decide their placement from the intended side after a resize or an updatePosition(), instead of building on the correction of the first paint, and a point-mode offset now carries the sign of the direction the popup grows.useIntersection() composable -> observe any element's visibility from setup() with a plain object, a ref or a getter (target, root, rootMargin, threshold, once, disabled, onIntersect), getting back a reactive isIntersecting and a stop(). The observer pool behind it is now shared by the composable, v-intersection and QIntersection (rebuilt on top of the composable), so a page mixing the three with the same options runs a single IntersectionObserverside prop, pull from any edge of the content to refresh -> the pull can start from the top (the default, unchanged), bottom, left or right of the content, as an inward pan from that edge while the scroll target sits at it. bottom is the messenger case, left/right serve horizontally scrolling content (#7599)cursor-position prop, position the tooltip at the pointer -> on a large trigger a tooltip placed against the anchor's box ends up far from what the user is pointing at, which is why the native title attribute opens at the cursor. The pointer becomes the anchor point (anchor no longer applies, self and offset keep working), after waiting for the pointer to settle so a tooltip opening mid-sweep can't freeze at a point already left behind; a touch or stylus press opens at the contact point without waiting, and a keyboard focus or a bare model toggle keeps the regular anchor-relative placement (#6019)overflow: hidden on desktop, so the only way to reach offscreen tabs was the arrow icons and a horizontal trackpad swipe or a wheel tilt did nothing. It now scrolls on its own axis on every platform, the cross axis staying hidden, the scrollbar staying invisible and the arrows still tracking the position. Vertical wheel over horizontal tabs is deliberately not remapped, so a tab bar in a page never captures the page scroll (#7404)stagger prop -> each action fades and scales on its own, cascading outwards from the trigger when opening and back towards it when closing, which is the Material speed dial behavior. stagger sets the milliseconds between consecutive actions (default 40; 0 animates them all at once); the delays are derived in CSS with sibling-index(), so no number of actions is special-cased and browsers predating those functions simply animate the actions together (#6413)ellipsis slot to replace the "..." buttons -> the ellipsis buttons were hard-wired to jump to the first hidden page on their side and there was no slot to attach anything else. The slot replaces the button entirely and hands over { side, page, btnProps, onClick } (plus to when to-fn is set); btnProps replicates the default button but carries neither the click handler nor the router target, so spreading it onto a QBtn hosting a QPopupEdit does not also trigger the jump. Without the slot the rendered output is unchanged (#11341)dropdown-hover, dropdown-hover-delay and dropdown-hover-hide-delay props are forwarded to the QBtnDropdown of every toolbar dropdown. Click, tap and keyboard interactions keep toggling as before, and touch devices fall back to themextractDate() parses ISO week dates -> the week (w/wo/ww), ISO week year (GG/GGGG) and day of week (d/do/E/ddd/dddd) tokens were emitted by the parser but never mapped, so a GGGG-[W]ww-E mask resolved to Jan 1st 1900 instead of the day it described and twelve formatting tokens could not survive a formatDate() round trip. A mask carrying a week number now resolves as an ISO week date, an explicit calendar day still winning. See the note at the top (#7732)v-intersection, QIntersection and useIntersection, clipping through every scrollable ancestor up to the viewport, and takes the fraction of the element that must be visible as an argument (v-scroll-fire:0.5, default 0). See the note at the top.mouse on a device without touch) can be armed later. The notes are gone from the four pagesscroll-target prop accepts a Vue component reference, standing for its root element -> QInfiniteScroll, QScrollObserver, QVirtualScroll, QParallax, QPullToRefresh, QTree and QTable share the prop, and the resolver behind it has always unwrapped a component instance to its root element, but the prop type rejected it: in production it worked, in development Vue's prop validation refused it and building the warning threw ("Cannot convert object to primitive value"), aborting the update. A value without a root element now falls back to auto detection with a warning naming the causequasar.config spinner of Loading and Notify is typed by name -> framework.config.loading.spinner and framework.config.notify.spinner take a spinner's name ('QSpinnerGears'), which the CLI imports for you, but the typing dropped both keys altogether, so naming a spinner there was a type error. quasar now also exports the QuasarSpinners union of the spinner component namesrootBounds from a cross-origin iframe for every entry of the viewport root, and a 2020 guard read that null as the element having been caught out of the DOM by a Vue transition, re-observing forever. So inside Codepen, jsFiddle and StackBlitz previews no v-intersection or QIntersection handler ever ran, since 2020 (the docs advised the root prop as a workaround and Codepen was disabled on the examples). Firefox was unaffected. The guard is dropped, the docs' workaround paragraph with it, and the Intersection examples get their Codepen link backmousedown and touchstart only, so a page cancelling pointerdown (Cesium does it on every mouse press with a registered action) suppressed the compatibility mousedown and the popup stayed open, in every engine. QSelect's dropdown got stuck along with it. A mouse or pen pointerdown now arms a 0ms fallback that the mousedown of a normal press cancels in the same task, so every existing path is unchanged (#12575)top/left, so a popup with a bottom or right self origin kept its top edge frozen: content growing after the decision extended it over its own anchor until the next pass re-derived the position and it jumped. The engine now writes its inset from the edge the self origin anchors, like the native onemax-height/max-width props, the stylesheet ceilings ($menu-max-height, ...) and any app CSS at once. Before the amendment a flip towards a roomier side could widen the popup past those bounds (#18536, #18537)center/middle had no viewport correction at all, and now shifts back inside the viewport, frozen at decision time. The offset was applied with a fixed positive sign whatever the self origin, so a popup growing up or left opened onto the coordinate instead of clearing it; it now carries the sign of the direction the popup grows. Visible today for a QMenu opened at a coordinate. See the note at the topdisable/readonly in the toolbar, keep the fullscreen root opaque -> getBtn() overwrote the editor-wide disabled state with the per-button definition, so every plain toolbar button of a disabled or readonly editor stayed actionable (only the dropdowns behaved), which is how a disabled fullscreen editor's fullscreen button could be clicked. readonly keeps the read-only commands (fullscreen, print, viewsource), disable takes the whole toolbar out of play. The global .disabled rule also sat on the root, which in fullscreen is a viewport-sized fixed element, so the page composited straight through it. See the note at the top (#15486)disable/readonly toggle, QTabPanels/QStepper/QCarousel destroyed every panel (KeepAlive state included) on a swipeable toggle, QDrawer re-created its content on a no-swipe-close toggle and QSplitter its separator slot on disable. The directive now stays attached and is disarmed in place. QColor's spectrum, QSlider/QRange's track and QDrawer's backdrop got the same treatment internally (#12668, #18535)isFinal payload never arrived: QSplitter kept its --active class and dropped the update:modelValue, QPullToRefresh stayed in its pulling state, QDrawer's close pan lost its end on a breakpoint crossingv-intersection accepted Boolean false only and threw on undefined, and v-mutation tolerated a non-function value only by accident, keeping the observer connected and rebuilding it on every handler change (an inline arrow function on each render). Both now disarm cleanly, v-mutation dropping its observer and only swapping the callback target on a handler change (#18531)inject() fallback factory was never flagged as one, so the function itself landed in $fab and a QFabAction rendered outside a QFab crashed instead of showing itself as an enabled, standalone button (#18538)v-morph.tween="{ name, group, model }" lost the tween. The directive modifiers are the base now and the object overrides only the keys it definesdeepEqual() compares binary data by content -> ArrayBuffers, DataViews and typed arrays were compared through the generic object path, and the typed-array branch was duck-typed on a .buffer property, so a plain { buffer: ArrayBuffer } (and DataView) spun forever. Binary views are recognised through ArrayBuffer.isView(), which also covers views over a SharedArrayBuffer, and both sides' byteLength is compared before anything is allocated (#18539)ComponentInstance, not a Component -> QFile, QForm, QPopupProxy, QScrollArea, QSelect, QTable, QTree, QUploader, QVirtualScroll and useVirtualScroll hand over mounted component instances that the generated typings declared with Vue's definition type. Component stays where a definition really is passed: the plugins' spinner options and Dialog's componentv-ripple, v-scroll, v-close-popup, v-intersection, v-morph and the four touch directives now share module-level listeners instead of allocating a set of closures per element, v-intersection shares one IntersectionObserver per distinct config (per-scroll main-thread time on 3000 observed elements 4.1ms -> 2.5ms) and v-scroll one listener per target (2000 elements on window: mount 8.9ms -> 2.0ms, unmount 8.3ms -> 0.8ms, one scroll event 1.74ms -> 0.08ms). QInfiniteScroll and QPullToRefresh watch the content through the intersection pool rather than a scroll listener and render their indicator in a sub-component, so a load or a pull no longer re-renders the slot content, and QPullToRefresh arms TouchPan only while the pulled edge is on screen, keeping JS out of the touch path of every other scroll gesture. Every template ref in the ui package is a shallowRef()useFullscreen() relocation (#18513)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:
Malformed entries are skipped, so a lang/*.js glob's deprecated files do no harm, and getLocale() can be passed straight in. The docs' dynamic boot-fi…
Important
One behavior change worth checking before you upgrade:
@click listener on QItem or QChip now implies clickable. The clickable prop became tri-state (default null): when you do not set it, the presence of a click listener decides. An explicit value still wins, so :clickable="cond" and clickable="false" are untouched. Markup that carried a previously dead click listener now gains the hover styling, focusability, the derived button/menuitem role and starts firing its handler.click from the handler they bind while clickable, so a @click on an item or chip without that prop was dead code. See the note at the top (#16731)getClosestIsoName() maps a locale to the nearest language pack -> language packs carry a region subtag only when a language ships more than one translation, so a browser locale like es-MX or sr-Cyrl-RS has no pack of its own and every app hand-maintained a locale-to-pack map. Lang.getClosestIsoName(locale, isoNames) (also on $q.lang) picks the closest of the packs your app ships: exact match, then the RFC 4647 lookup chain with the script outranking the region (sr-Cyrl-RS reads sr-Cyrl, zh-Hant-TW reads zh-TW), then the CLDR likely subtags (zh reads zh-CN, pt reads pt-BR, de-AT reads de-DE over de-CH). Malformed entries are skipped, so a lang/*.js glob's deprecated files do no harm, and getLocale() can be passed straight in. The docs' dynamic boot-file recipe now uses it (#12174)blur and ran lazy validation (an empty date input went red just for opening the picker). QMenu and QDialog dispatch bubbling popup-show/popup-hide DOM events again, the way Quasar v1 did, and QField holds its focused state, styling and lazy validation until the popup hides. Hover-shown menus and tooltips take no focus and stay silent (#9779)position: fixed dialog is attached to, and it reveals the focused field only for a user-initiated focus, so a field focused programmatically (autofocus or a .focus() call) opened the keyboard with a bottom-anchored dialog left entirely underneath it. QDialog now tracks the visual viewport while shown on iOS and insets its inner element to it. Verified on the iPhone 17 Pro simulator (#17594)100dvh now and the platform override is goneoffsetWidth, which rounds each one up, so three justified tabs in a 440px container summed to 441, scrollable flipped on, the tabs collapsed to their natural width and nothing re-measured. It repeated at every third pixel of width, which is the on/off alternation of the report. Tabs are now measured with getBoundingClientRect() and an overflow of a pixel or less is never treated as scrollable (#18532)offsetHeight as 29, so the paddings standing in for the items outside the slice put the content half a pixel per item off on every re-slice and the rows visibly jittered while scrolling, worse the deeper into the list. Both directions measure through getBoundingClientRect() now (#15754)$q.screen and the CSS breakpoints. A standard layout now reports $q.screen.width; containerized layouts are unchanged (#15506)disable is toggled -> all three baked !disable into the root vnode key, so flipping the prop destroyed and rebuilt the whole subtree instead of leaving the content as it was. Disabling now travels through the directive value. One related change: with QIntersection's once, an observer that has already fired is no longer re-armed by toggling disable off and on, which was a side effect of the remount and contradicted once (#12668, #18531)overflow: clip, which is not a scroll container; layout is unchanged (#17464)ssrContext -> Node (and Vitest) resolve quasar through the node export condition to the SSR build, so a test setup that does not go through @quasar/vite-plugin installed it and crashed with "Cannot convert undefined or null to object". The error now explains what happened and how to fix it, and the Vite plugin docs gained a "Testing with Vitest" section (#15469)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:
Important Several behavior changes worth checking before you upgrade:
Important
Several behavior changes worth checking before you upgrade:
scroll-target prop. Popups now hear every scrolling container, nested ones included, through a single capture-phase document listener, so no container needs to be designated (and the scroll helper 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.updatePosition() now means "re-run the placement decision"; a plain anchor move needs no call at all.fit/cover minimum sizing follows the real anchor box and is no longer inflated by the offset prop.Platform.is flags ie, edgeChromium, winphone, kindle and silk are gone (the first two were never set on any browser Quasar supports, the rest were dead platforms). is.edge now plainly means Chromium Edge.: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.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 the scroll class" limitation (the #8343 family)hover, hover-delay and hover-hide-delay props, 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; @filter selects still load their options on hover. A real click or Tab upgrades the show to a regular focused open (#11675)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 the color prop, styleable through q-select__dialog-close, opt out with the new hide-dialog-close prop (#6858)no-chip-remove prop -> with use-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)no-option-label prop -> a plain no-options message without having to reach for the no-option slot (#9463)q-select__selected-value class as an officially targetable hook, e.g. white-space: pre-line to render embedded newlines (#17973)lazy modifier -> 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)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 a hide() call. Custom components built on useDialogPluginComponent get it with no template changes. New exported TS type: DialogDismissReason (#17938)onCancel/onDismiss -> the same argument, 'backdrop', 'escape' or 'programmatic'scroll-target prop stays with its other, narrower meaning: the box the scroll percentage is computed againstengines field (node >= 10!) move to Node 22<KeepAlive> never received its transition-end, so it came back stuck half-faded (#18201, #18526)once + ssr-prerender, freshly hydrated content was destroyed and rebuilt on every page load (#17099)body-cell-<name> / header-cell-<name> slots were dropped unkeyed into the row, so changing visible-columns made 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 for top-row/bottom-row and top-selection (#16047)sortOrder: 'da' showed an up arrow even though the first click sorts descending (#18009)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)scrollTo() -> a scroller with CSS scroll-behavior: smooth animated the internal repositioning (#18168)hide-scrollbar on the root element -> since Chrome/Edge 121 honor scrollbar-width, and it only reaches the viewport scrollbar from the root element, the anti-flicker resize logic had quietly stopped working (#17122)naturalWidth/naturalHeight from the img's current laid-out box, so when the load event lost the race with layout the wrong ratio got locked in and object-fit: cover cropped the image (#15652)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 compositiontoolbar prop rendered fine but crashed on entering source view (#16940)validate() returns nullish instead of breaking (#17921)hide() -> the hide/before-hide events carried no payload on ESC dismissal, so listeners could not tell a keyboard dismissal from a backdrop onearia-activedescendant was never set there either, breaking the combobox pattern's active-descendant navigation (#16599)body instead of returning it to the control (#6858)behavior="dialog" and use-input, typing into the focused but closed control was a dead end: the text piled up while the filter early-returned (#15976)@filter options and map-options, a preloaded model value displayed raw until a user interaction loaded the options. QSelect now fires that one filter call itself (#17983)@popup-show/@popup-hide -> both events always received undefined (#14678)no-option slot) left the typed filter text behind in the input (#16135)cover with transition-show) for JSX/TSX consumers and the docs API card listed none of them. separate-close-popup is the one documented exception: the menu branch overrides it (#18530)q-focusable/q-hoverable helpersbody.desktop, so the keyboard navigation QSelect just gained on mobile moved a focused state nothing painted (#16599)escape-key event, and QVirtualScroll's entirely undocumented QList / QMarkupTable attrs forwarding is now declared. Both reach the generated TS types and the docs API cardserror prop reserves the bottom space even when false -> the false vs null distinction (reserve the error line vs no external validation at all) was undocumented and kept getting reported as a bug (#17662)<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 themQuasar 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:
The old import paths keep working as deprecated aliases for the whole of Quasar v2, but code comparing $q.lang.isoName against the old values needs up…
Important
Two behavior changes worth checking before you upgrade:
isoName: kur-CKB reports ckb, mm reports my and sr-CYR reports sr-Cyrl. The old import paths keep working as deprecated aliases for the whole of Quasar v2, but code comparing $q.lang.isoName against the old values needs updating.hover, hover-delay and hover-hide-delay props, the same contract QMenu and QBtnDropdown got in v2.26. The pointer can cross the gap between the main button and the actions without closing them (150ms grace period by default), touch devices keep tap-toggling, keyboard interaction is untouched, and focus is never movedcol-name prop -> a QTd or QTh wrapped in your own component inside QTable's #body slot can finally say which column it belongs to. The cells used to resolve their column from the vnode key, which Vue never forwards through $attrs, so a wrapped cell rendered nothing. Existing key="..." usage keeps working; col-name takes precedence (#17830)quasar/lang/ckb, quasar/lang/my and quasar/lang/sr-Cyrl for Central Kurdish, Burmese and Serbian Cyrillic. kur-CKB, mm and sr-CYR remain available as deprecated aliaseshover menu's target and clicking it in one gesture closed the menu the hover had just opened. Also applies to QBtnDropdown (#18524)key set on the grid item slot content -> the mapped rows reached Vue as keyless Fragments, so removing a row recreated every card after it instead of moving the surviving instances (#18045)right inset could not be flipped by the RTL stylesheet (#17943).q-electron-drag regions in Electron -> menus, dialogs, notifications and tooltips overlapping a draggable title bar lost the overlapping area to window dragging. An explicit drag class on the content itself still wins, and dialogs keep letting you drag the window (#17285)fill-mask="0" and reverse filling, the unmasked value grew by one fill char per keystroke ("0.01" became "00.01", "000.10", ...), which is the docs' own "Price with 2 decimals" example (#18523)unmasked-value and forward filling, a typed "1" over ###-## with fill-mask="0" was reported as "10000"; with mask AA-AA and fill-mask="A", "X" became "XAAA" (#18523)reverse-fill-mask, every mask mixing letters and digits silently rendered nothing for ordinary values ("ab12" over AA-##), including values the control had just rendered itself (#18523)AA-## rendered "-57", which unmasked back to "57", so what the field displayed was not a value it could read again (#18523)sr pack, Simplified characters inside zh-TW, untranslated English left in translated packs, and year-range plural/agreement errors in some 40 languages (#18522). Ukrainian was corrected first in PR #18522 by Ihor Diachenkooffset prop of the position engine does not translate the popup, it inflates the anchor box, so middle/center origins are offset-invariant by construction. That is why QTooltip's horizontal offset reads as broken on its default anchor="bottom middle". Documented on the API of QMenu, QTooltip, QBtnDropdown, QPopupEdit and QSelect, with the direction each offset component expands in, plus the note that QMenu's cover drops the offset entirely (#18212)validateStringArray was declared with four parameters and called with three, so its loop never ran. Every string, array entry and formatter result is now checked for padding, doubled spaces and control characters, and formatters are probed across the counts that select their plural branchesQuasar 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:
Important A few behavior changes worth checking before you upgrade:
Important
A few behavior changes worth checking before you upgrade:
placeholder attribute (it was not valid on a div). Custom CSS targeting .q-editor__content[placeholder] must switch to [aria-placeholder]..q-field__native wrapper. Selectors or tests that matched the wrapper copy (or relied on the attribute matching two elements) need updating.change fires only when an interaction actually moved the value, and a parent that does not sync the model prop back hears each value once per interaction rather than once per press/release/click.:-webkit-autofill to the standard :autofill, so the q-autofill keyframe and $input-autofill-color apply there for the first time.hover, hover-delay and hover-hide-delay props, also forwarded by QBtnDropdown (in split mode hovering either button opens it). A hover-open never steals focus from where you are typing, ESC still dismisses, submenus keep the chain open while the pointer travels between them, and touch devices keep tap-toggling. No effect when context-menu is set (#6776)min-range and max-range props -> constrain the width of the selection. Out-of-limits model values are coerced like inner-min/inner-max already do, dragging and the keyboard clamp each thumb to what the other thumb allows, and a minimum width blocks thumb crossover (#18182, #2100). Based on PR #18182 by Teodor Atroshenko (thexeos)footer slot -> renders a real <tfoot> element, so a totals/summary row can be made sticky through CSS. Works in default and virtual-scroll modes (not in grid mode) (#7570)getISOWeekYear() util and the Moment/Day.js-compatible GGGG/GG format tokens, so masks like YYYY-[W]ww no longer render the wrong pairing at year boundaries (Jan 1st 2022 was formatted as 2022-W52 instead of 2021-W52) (#17088, credit: psevertson)print-color-adjust), Firefox gains the QField autofill styling (:autofill), the QFile/QUploader file button styling (::file-selector-button) and the QPagination input styling (appearance), while prefixes no engine reads any more are goneclass, style, key and ref on components in JSX/TSX -> the exported component types did not carry Vue's PublicProps, so TSX rejected the universal props. Templates were never affected (#8690)pointerType whether it hovers or toucheschange only when the interaction changed the value -> pressing End with the thumb already at the maximum, clicking the value the slider already had, or dragging away and back all fired a spurious changechange only when a user adjustment lands a new value -> mounting, every parent model write and range clamping used to fire it, contradicting its own API descriptionstep drifted (0.1 + 0.1 + 0.1 = 0.30000000000000004), so marker-labels rendered the drifted number and marker-label-* slot lookups misseddisable while a Dialog or overlay Drawer held the scroll lock never polled again (#18520)done() triggered the next load forever (and reverse mode jumped the page behind the overlay to the bottom on mount). Such a placement now warns once, points at the scroll-target prop and revives itself if the fixed positioning goes away (#18520)aria-label matched two elements and broke strict single-element queries (Playwright getByLabel, Testing Library getBy*); the wrapper copy was invalid ARIA as well (#18519)name/for set but a null or empty model rendered no element at all, so querySelector('[name=x]') could not find it. Form submission and validation are unaffected (#17951)aria-placeholder the component already renders, and the invalid placeholder attribute is no longer emitted (#18511)role and aria-* attributes to the aside element -> they landed on the inner scrolling div, so naming or re-typing the landmark through QDrawer was impossible (#18059).q-textarea pinned its rows to 18px, so a bigger font gave cramped, overlapping lines that apps had to override with !important. The default layout is unchanged (#16772).q-router-link--active) or custom-colored item kept gray secondary text. Derived from currentColor via color-mix() at the same emphasis levels, with the legacy values kept as fallbacks (#18300)switch-toggle-side (#18008)events and options array forms (and the String handed to their function forms) are always compared as YYYY/MM/DD -- the mask prop never applies to them. Documented on the API instead of changing it; the function form remains the customization point (#16735)behavior, breakpoint, show-if-above, persistent and no-mini-animation were documented by example only. Adds a "Desktop and mobile behavior" section and corrects show-if-above, which was described as initial-render-only when it actually re-shows the drawer every time the layout goes back above the breakpointQuasar 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:
Important A few behavior changes worth checking before you upgrade:
Important
A few behavior changes worth checking before you upgrade:
lazy-rules now clears an error while you type: while an error is displayed, the field re-checks on each model change instead of waiting for the next blur. Rules that were only ever meant to run on blur will now also run on every keystroke for as long as the field is in error..q-field--with-bottom's padding-bottom or on the absolutely positioned .q-field__bottom--animated needs a second look.@scroll: a scroll update suppressed while the body scroll lock was held is now applied when the lock releases without restoring the position.lazy-rules re-validates on model change while an error is displayed -> the error clears as soon as what you type becomes valid, instead of staying up until the next blur (#17456)validate() throws synchronously -> that child now fails and gets focused, exactly as a rejected async rule already did, instead of the exception breaking the whole submit()hide-dropdown-icon -> with no icon to swap for, the default spinner appended net-new width and the field jumped on every filter round-trip. The default spinner is now suppressed when the dropdown icon is hidden (an explicit loading slot still renders), and the clearable icon keeps its place while loading instead of vanishing (#17375)align="right" -> Blink and Gecko keep start-side overflow out of the scrollable overflow region, which collapsed the measurement and hid both arrows, leaving the overflowing tabs unreachable by pointer (#17847)@load in a loop until the whole list had been fetched (#13257)11## swallowed a typed "1" and 04## ### ### a typed "0", because the char could not be told apart from the literal sitting at that position (#15624, #18051)show/hide through arbitrarily nested components -> a QDialog sitting behind two or more single-root wrapper components failed with "Incorrectly defined Dialog component" and never opened (#18159)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:
Important A few behavior changes worth checking before you upgrade:
Important
A few behavior changes worth checking before you upgrade:
role="menu" and QBtnDropdown's toggle no longer emits aria-haspopup -- a menu may only contain menuitem* children, so the old markup was invalid. Declare role="menu" on the wrapped QList (its actionable QItems then expose themselves as menuitems automatically) and use the new toggle-aria-haspopup prop on QBtnDropdown.colorPicker and uploader sections plus several keys. All are optional in the types and read with optional chaining, so existing packs keep working and compiling -- but they will announce nothing for those internals until updated.virtual-scroll prop family, @virtual-scroll event and key-based scrollTo(key, edge). Only the rows around the viewport exist in the DOM, so cost stays flat however much of the tree is expanded (22.6k nodes, dev mode: expand-all 2.2s -> 23ms, filter keystroke on a fully expanded tree 560ms -> 29ms, ~600 DOM elements at any scale)getIndeterminateNodes(), isIndeterminate(key), getParentNode(key) and tri-state getTickState(key) (returns the true/null/false a QCheckbox takes as its model); the header and body slot scopes gain an indeterminate boolean next to ticked (#17298) (#12762)options prop and the navigation-min/max limitsslider role now sits on the focusable element, and the whole family gains Home/Endleft-thumb-aria-label and right-thumb-aria-label propstoggle slot -> renders next to the dropdown arrow, so content like a QTooltip can finally attach to the toggle button in split mode (#16955)toggle-aria-haspopup prop -> names the popup's role on the control that actually opens it, in both the regular and the split designrole prop -> the documented override path (e.g. menuitemcheckbox, or role="menu" on a QList)$layout-marginal-background and $layout-marginal-color Sass variables for the marginal sections (#10476)escape-key eventcontrol slot scope gains ariaInvalid, ariaDescribedby and ariaErrormessagecolorPicker and uploader lang sections plus table.selectAllRows, table.selectRow and pagination.label, translated across all 72 language packsaria-pressed and mark today with aria-current="date"; the view switcher becomes a real button that Space activatesaria-disabled was set only for radiosseparator; arrows move it by 1% (10px in px unit) and Home/End jump to the limits (#12466)separator-aria-label proptabpanel role lives solely on the panel components (#6560)tabpanel role and becomes a group labeled with its title, conveying aria-current="step" even with header navigation off; QCarousel's navigation is exposed as the tabs pattern it actually is (#6560)role=toolbar with a localized name, a single Tab stop with roving tabindex, arrow navigation (wrapping, RTL-mirrored) and Home/End (#15675)aria-pressed instead of color alone, and the hyperlink editor's URL field gets a namerole=menu (#17162)aria-expanded/aria-controls stay in sync without apps mirroring the open state by handaria-modal dialog -> menus anchored in a modal QDialog (a QSelect's listbox included) were pruned from the accessibility tree and never announced (#17078)listbox role wraps only the options, which now carry aria-setsize/aria-posinset (#17344)<q-input error hide-bottom-space> pointed aria-errormessage/aria-describedby at a missing idaria-hidden control (#17810)carousel lang-pack section, translated across all packs (#7423)v-show (22.6k-node tree, dev mode: 2196ms/299,713 DOM elements -> 31ms/169), a state change re-renders only the affected nodes, and the monolithic O(N) meta rebuild became layered derivation that recomputes only on its own triggers. Per-key state of keys that left the model is pruned, and getTickedNodes()/getExpandedNodes() use keyed lookupsduration <= 0 short-circuits to an immediate settleuseQuasar() resolves through the instance's global properties (~0.6ns/read vs ~37ns), size styles are module-cached, and trivial computeds were flattened out of the render path across ~70 files (QBtn, QItem family, the checkbox family, QChip, QTab/QRouteTab, QList, spinners and 12 bulk-mounted leaves, plus use-dark/use-align/use-form/use-transition/use-router-link/use-split-attrs)formatDate masks once and cache per mask@scroll payload assemblyisKeyCode, loop-based sumSize, regex-free hexToRgb and allocation-free mask token testersshouldIgnoreKey()@keydown.ctrl.b.prevent could observe the key but not stop QEditor toggling bold<a href="https://"> straight away, leaving a broken link behind if you walked away (#18289):class on an open QDialog never reached the portal root, and a dynamic id went stale on QTooltipposition: fixed descendant anchored itself to the scroll area and was clipped there (#12109)fill-input (#17763)autogrow is toggled off (#17535)direction="up" and labelposition="right"label-position="top"/"bottom" laid icon and label out in a row instead of a columnhideSelection prop from the alpha sliderconfig > screen > bodyClasses key on SSR, which never emitted its body class into server-rendered HTMLuseQuasar()'s inject() fallback past the no-instance case[Accessibility] category in the Specs workflowQuasar 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:
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →