github quasarframework/quasar quasar-v2.32.0

4 hours ago

Important

Behavior changes worth checking before you upgrade:

  • QImg now renders its <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.
  • The .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).
  • QExpansionItem and QTree no longer render a QSlideTransition around their content; they drive the slide on the content element themselves, so the two extra component instances per item (per tree node) are gone from the tree. Their duration prop is now a real Number prop on the component itself rather than a passthrough to the child.
  • On Chromium, QSlideTransition, QExpansionItem, QTree and the vertical QStep no longer write inline 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.
  • QInput autogrow textareas are sized by the browser through CSS 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.
  • QParallax registers no scroll or resize listener and no IntersectionObserver where it can ride a view timeline (currently everything but Firefox). The 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.

New

  • feat(QImg): render the image in the SSR HTML; new prop -> 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 in
  • feat(QDrawer): pan 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 back
  • feat(QParallax): refresh() 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 three

Other

  • perf(QParallax): the media moves on a view timeline where the browser has one -> the media now rides an Element.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 box
  • perf(QSlideTransition): slide on a Web Animation where calc-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.3ms
  • perf(QExpansionItem/QTree): slide the content directly, without QSlideTransition -> both components own their content element and its visibility, so the QSlideTransition + Transition pair per item (two component instances, per tree node) only relayed the slide hooks to a v-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.3ms
  • perf(QInput): size autogrow textareas with CSS field-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 cases
  • refactor(ui): size QImg, QResponsive and QVideo ratio boxes with CSS aspect-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 top
  • perf(use-position-engine): resolve the CSS anchor verdict once at import -> a QMenu or QTooltip setup now reads a boolean constant instead of calling into a cached support probe

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:

Don't miss a new quasar release

NewReleases is sending notifications on new releases.