Important
Behavior changes worth checking before you upgrade:
- A readonly QInput/QSelect/QFile/QField now reflects focus: it shows the focused styling (highlight, floated label,
hide-hinthint), emitsfocus/blurand validates on blur withlazy-rules. Onlydisablenow exempts a field from counting as focused, matching the existing rule that onlydisableexempts 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 lightfalsesince 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.- The
faandfa-IRlanguage packs render Persian digits in QDate, QTime and QPagination, through the new root-levelformatNumberpack key. - QStepper's horizontal DOM lost its
.q-panel.scrollwrapper element; its classes moved onto.q-stepper__step-content, now a direct descendant of.q-stepper__step. - The
q-transition--field-message-*classes no longer exist. Restyle a QField message swap through.q-field__messages--animatedinstead; the leave half of the animation (the old message fading out under the new one) is gone.
New
- feat(QColor):
paletteslot, 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 scopedpaletteslot replaces the default swatches while keeping the other views and the view switcher, handing over the resolved colors (palette), aselect(color)function and aneditableflag. 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.saturationandcolorPicker.brightness, translated across every pack (#8613) - feat(ui): shadow colors are CSS custom properties -> since v2.11 the elevation shadows are drawn with a light color while in Dark Mode, but the color was baked into the built CSS through the
$shadow-color/$dark-shadow-colorSass 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:rootas--q-shadow-colorand--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 });transparentremoves the shadows altogether. The tints are derived onbody, so the tokens are read at the:root/bodylevel (abody.body--darkrule,setCssVar()), not on a deeper element (#15144) - feat(Lang):
formatNumberpack key, localized digits in QDate, QTime and QPagination -> a Persian calendar coupled with thefa-IRpack 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 thelocaleprop) can now define an optional root-levelformatNumber(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-labelthrough it, QTime its header units and clock face, QPagination its page buttons and theiraria-label. Model strings, date hashes and QTime'sstringModelstay ASCII. Thefaandfa-IRpacks opt in; every other pack is unchanged (#12735) - feat(QTable):
autoWidthin the column definition -> shrinking a column to its content required a per-columnheader-cellandbody-cellslot override with<q-th auto-width>/<q-td auto-width>, cumbersome for dynamically built columns, the only alternative being an undocumented class throughclassesandheaderClasses. A column can now simply declareautoWidth: true, which applies to the auto-rendered cells as well as to QTh/QTd fed with the slot props (#8206)
Fixes
- fix(QMenu/QDialog): the portal opening over a closing one takes the focus -> a QMenu item or a QDialog button carrying
v-close-popupthat 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) - fix(QField): a readonly field reflects focus like a native readonly control -> the focused state was gated on the field being editable, so a focus event landing while
readonlywas still true got dropped and nothing re-synced afterwards: a handler flippingreadonlyoff and callingfocus()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, onlydisablekeeping a field from counting as focused. See the note at the top (#16056) - fix(QField): forward focus from the
controlslot wrapper to the control -> with acontrolslot, autofocus,focus()and every other way of focusing the field (QForm/QDialog/QMenu autofocus,clearValue, a click around the control) landed on theq-field__nativewrapper: the field looked focused and emittedfocus, 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) - fix(QField): no more one compositor layer per field on Chromium -> every QField label carried
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) - fix(QSelect): typed text wins over the model's automatic highlight on Enter -> opening the list (and every filter completion) highlights the option mirroring the current value. With
new-value-mode/@new-value, Enter or Tab then re-selected that option and silently dropped the text the user had just typed,@new-valuenever 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) - fix(QStepper): the steps stay mounted across an orientation switch -> a stepper bound to
$q.screen(:vertical="$q.screen.lt.sm") lost the active step's content, form state included, whenever the viewport crossed the breakpoint, andkeep-alivecould 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'stransition-durationalso drives the vertical slides (default unchanged). See the note at the top (#9703) - fix(QStepper): a nested stepper keeps its own orientation -> the horizontal and vertical layout rules hung off the stepper root with descendant selectors, so a stepper nested inside a step of the other orientation matched both rule sets and source order decided: a vertical stepper inside a horizontal one drew a horizontal connector after its active label, lost its vertical connectors and got clipped by the tab overflow; a horizontal one inside a vertical stepper lost the connector before each dot and received the vertical content padding. Every orientation rule now hangs off a per-element modifier class instead of the root; no selector gained a class and several lost one, so existing overrides keep their targets and their precedence (#16516)
- fix(QStepper): show the prefix on an active erroring step with
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 haderror, withactive-iconset tonone, got neither the icon nor the prefix and the dot was left empty; a step-levelactive/error/done-iconofnonehit 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) - fix(QDrawer): mini-mode items stay put when the standard layout takes over -> in mini mode the item's side section was flex-centered inside the drawer's content box, which only agrees with the standard layout's 16px anchor when that box is exactly 56px wide. An unbordered drawer gives 57px, and on a display whose scale factor is not a whole number Chromium and WebKit snap the 1px border to a fraction of a pixel, so the centered icon landed on a fractional position and visibly jumped once the mini animation restored the standard layout (Mac Retina and 1x displays are exact, which is why it could not be reproduced there). The drawer now hands its current width to CSS as the
--q-drawer-widthcustom 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) - fix(QDrawer): iOS Safari's edge navigation swipe stays out of the drawer gestures -> on iOS Safari, swiping in from the screen edge to open a drawer also ran the browser's back/forward navigation gesture: the page slid towards the previous history entry while the drawer followed the finger. The same happened when the backdrop of a right-side drawer was swiped from the left edge to close it. The opener strip and the backdrop now cancel the touch sequence once the pan direction is detected, which is what leaves the page in Safari's hands, while a vertical move (and so the page scroll) is left alone
- fix(prevent-scroll): the page background keeps painting under the iOS scroll lock -> the iOS lock pins the body with
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 elementmin-height: 100%,min-heightso userhtml { height }rules still win (#16799) - fix(QResizeObserver): track padding and border changes of the observed element -> the component emits
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 apadding-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 ownq-ios-padding/q-safe-area-paddingrules use, was already tracked (#7526) - fix(QPage): the children's vertical margins stay inside the page -> QPage was a plain block with no padding or border, so a first child's top margin (a
q-mt-mdcard, say) collapsed through it and pushed the page down inside QPageContainer; together with the page'smin-heightthe 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) - fix(Dark):
'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, soconfig.dark: 'auto'ended up as a plainfalseon 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) - fix(debounce): guard reentrant immediate calls -> a debounced function created with the
immediateoption could re-enter itself from within its own invocation and fire again instead of being held back for the wait period (#18544) - fix(morph): give up on a destination detached before the first frame ->
morph()defers its work to arequestAnimationFramecallback, and when the destination's subtree was removed in between (av-ifor an unmount in the same tick as the model change) the validity check still accepted it, because a detached subtree keeps itsparentNode, and the fixed-position parent walk then reachednulland threw insidegetComputedStyle
Other
- perf(QField): message swaps animate with a CSS keyframe instead of a Transition -> every field with a hint, rules, a counter or an explicit
errorprop 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) - perf(ui): 2D transforms are written as 2D instead of the 3D variants -> Chromium gives any 3D transform function its own compositor layer, permanently, and with hundreds of instances on a page every repaint re-runs the layer assignment against all of them. The production stylesheet never shipped these (the minifier flattens them), but
quasar devcompiles 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 minifiedquasar.prod.css/quasar.rtl.prod.cssare byte-identical before and after. QLayout's layer hack and QParallax's media transform stay 3D on purpose
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: