liquid_glass_easy 4.3.0
liquid_glass_easy: ^4.3.0 copied to clipboard
Apple iOS Liquid Glass for Flutter: a shader lens that refracts what is behind it, with blending, morph, adaptive chrome, controls, batching and lite glass.
4.3.0 #
LiquidGlassStyle.liteGlassis the rim's pickup, andLiquidGlassPickupis nowLiquidGlassLitePickup. A lens drawn as lite glass — by its style, the engine switches, or while the shaders load — always used one pickup; the style now names it in the same field that asks for lite:liteGlass: LiquidGlassLitePickup.surfacedraws this surface lite with thesurfacerim,null(the default) leaves it to the engine switches, and a lens those put on lite takes its rim from the newLiquidGlassEngine.litePickup,backdropby default.liteGlass: truebecomesliteGlass: LiquidGlassLitePickup.backdrop.mergetakes the override's when set, likeshape. A dialog, a sheet, a FAB or a button asks forsurfaceorbackdropthrough the samecopyWiththat tints it; the dialog, sheet and FAB example pages draw every surface this way, with thesurfacerim, and the lite page's lens follows the same chips as its card.LiquidGlassLitePickup.backdropis the default and keeps the tint out of its read;blendshows the light colour;surfaceis the bare overlay. The overlay rim left white alone, so over a pale surface a shape'slightColordid nothing — the slider's and switch's grey-white glint came back pure white under lite glass.blendnow multiplies the rim's own colour in after the overlay and the floor (one more pass over the same mesh, skipped when that colour is white), so a grey light is a grey rim and aborderColorshows;surfaceis the same rim painted over the tint and the child instead of under them, for when the rim should take the finished surface's colour.backdrop's read used to sit on top of everything — frost, tint, child — with the rim's own colour thrown away; it now goes on right after the frost, over a newLiquidGlassLite.color(the glass tint,LiquidGlassAppearance.color's counterpart) laid beneath the rim in the same layer, and under the child, and its light multiplies the clamped highlight as the shader's does. With a painted rim the tint is painted around the rim, cut by the rim's alpha, so the rim blends against the background behind the glass and not against the tint, and the tint never lays over the rim's core. A lens under lite glass hands its appearance colour to the lite and takes its rim from its style'sliteGlasspickup, or the engine's.- Lite glass honours
lightColorin its backdrop-pickup rim.LiquidGlassLitewithLiquidGlassLitePickup.backdrop— what every lens draws underLiquidGlassEngine.liteGlassOnSkia/liteGlassOnImpellerandLiquidGlassStyle.liteGlass— cut its rim out of the brightened background and threw the rim's own colour away, so a shape'slightColordid nothing: the slider's and the switch's grey-white glint came back pure white. The mask now multiplies the light into the clamped highlight, which is the order the shader applies it in, so a grey light is a grey rim. Same passes, same one read. ClassicBorderis deprecated. The classic rim is drawn: a sweep painted from a light angle, carrying its own shadow colour and its own one- and double-sided specular terms, sitting on glass whose every other facet is derived from the shape.OpticalBorder— the default since it arrived — takes the rim from the shape's own signed-distance field and picks its colour up from the background, which is what the material means by an edge. The move is not parameter-for-parameter and is not meant to be:shadowColor,oneSideLightIntensityanddoubleSideLightIntensityhave no optical counterpart, because the optical rim has no separate light to aim. Shape it withborderSaturation,ambientIntensity,borderSolidityandlightSpreadinstead — and note thatborderWidth,lightIntensity,lightColorandlightDirectioncarry over untouched, since they live onLiquidGlassShaperather than on the border type.ClassicBorderkeeps working for now and will be removed in a future release.LiquidGlassBlendertakessmoothness: 0, andLiquidGlassGroupis deprecated because of it. Turning the metaball off used to be spellednull— the only value that meant "do not fuse" — and0tripped an assert, which left the hard union reachable only through a second widget whose entire job was to default that parameter tonull. Zero now takes the same branch: the shader runs none of the smooth-union machinery, no smin, no per-member influence weights, no blended gradient, and the members share one surface, one backdrop read and one material while keeping their own outlines.nullstill means what it always meant and nothing on 4.2.0 breaks, but it is redundant, and new code should say0. Note that zero is a switch and not a small radius:0.001still takes the metaball branch, where the weights collapse to a 0/1 indicator and two overlapping members average their colours with a hard step at each outline — the tie zero does not have.LiquidGlassGroupis therefore deprecated: it isLiquidGlassBlenderwith a different default, every other parameter has the same name and the same meaning, andLiquidGlassGroup(...)becomesLiquidGlassBlender(smoothness: 0, ...)— a radius, if you passed one, carries over unchanged. It keeps working for now and will be removed in a future release.LiquidGlassShaders.ensureLoaded()compiles every shader, the blender's included. The merged-surface entry shaderLiquidGlassBlenderandLiquidGlassMorphdraw with lived in a cache private to the blender, so nothing could preload it, and a blender on the first screen painted nothing until its program arrived — not even the frosted fallback a lone lens shows. It now lives inLiquidGlassShadersbeside the lens programs, and the one line inmain()compiles all three ahead of the first frame; there is no second call to remember. Inside the package a lens still waits only on its own two programs, so a first lens is never held back by one it does not draw with.- On Skia, a scaffold with a glass-pill tab bar no longer captures the screen twice at rest. The glass-pill
LiquidGlassTabBarruns two capture pipelines: an inner view whose capture is the body, which the bar's capsule refracts, and an outer view whose capture is the whole screen — body, capsule, cells — which only the moving pill needs, since it refracts the bar it travels over. That outer capture already slept once the pill had settled, butLiquidGlassScaffoldput its whole chrome — app bar, side action, floating button,lenses, the system-bar strips — in the outer view, so an app bar alone was enough to keep it awake on every frame for as long as the screen was up. The scaffold now hands every slot — thedialogincluded — to the bar as its chrome (LiquidGlassAnimatedNavBar.chromeChild, passed throughLiquidGlassTabBar.buildGlassPillBar), and the bar places it by renderer. On Skia it rides the inner view, where it refracts the body on the capture the bar already takes, joins the same chrome batch, and asks nothing of the outer view; the outer view holds the moving pill and nothing else, so at rest the scaffold takes one full-frame capture, not two, whatever chrome it carries. On Impeller nothing changes: there is no capture to spare, so the chrome stays in the outer view over the bar, exactly as before. Layering on Skia changes accordingly: the bar's cells and its pill paint over the slots there, where they used to paint under them. An app bar, a side action and a floating button never meet the bar, so they look the same as before; alensesentry overlapping the bar sits under its cells on Skia; and on Skia thedialogbarrier dims the body and the bar's capsule but not the bar's cells or its pill. - The scaffold's sheets and dialogs can share the tab bar's read.
LiquidGlassScaffoldnow owns one chrome batch key and hands it to every view its chrome is spread over: on the glass-pill path the bar's capsule and the outer slots sit in different views and used to be two batches, and ashowLiquidGlassSheetorshowLiquidGlassDialog— a route, outside every view — always took a read of its own. Both presenters take a newbatchflag: with it they pick the key up from the context they are called with and publish it over what they present, so a sheet or a dialog opened from inside a scaffold costs no backdrop read beyond the one the tab bar already takes. It is off by default — a sheet or a dialog keeps its own read, as before, since batched it cannot see any other member's glass, and the glass inside it (a dialog's buttons) reads the page rather than its surface. The scaffold's owndialogslot is in the chrome batch already.batch: falseon the scaffold switches all of it off. Impeller only: on the Skia / Web capture path the flag is inert. - New —
LiquidGlassMorph: glass that morphs to fit whatever you put in it. Swap the child and the glass measures the new one and flows to its size;Keyis the identity, the child's own layout is the size, andwidth/heightare overrides that pin an axis rather than inputs. It is not a resize: the morph is two blobs of one liquid drawn as a single surface byLiquidGlassBlender— the destination blob carries the new shape and content, the source the old, and the smooth union joins them, so mid-morph the outline has a waist. Growing, the new blob's centre leaps ahead and its size catches up, pulling a neck out of the source; shrinking is the same film run backwards; each blob keeps its own corners; content blurs out and materialises in, pinned where the glass will finally sit; at rest the union is off and the outline is the plain shape.alignmentdecides which edge holds — andLiquidGlassMorph.alignmentFor(rect, field)recovers it for a surface placed by something else. Motion is a preset —LiquidGlassMorphMotion.fluid(default),anchoredPop,droplet,calm— over a spring, astretchand ananchor, with the set-once numbers inLiquidGlassMorphAdvanced; a fifth,plain, is a single lens whose outline springs from the old shape to the new (no second blob, no neck, one backdrop pass), andblended: falsemakes any preset that kind. The content cross-fade runs on the morph's own clock —LiquidGlassMorphMotion.duration, the spring's period — at the fractionscontentOutEnd,contentInStartandcontentInEndname, so "the new child shows at 0.3" means 0.3 of the way through the glass motion whatever the spring; a swap reversed mid-flight carries each child on from wherever it had got to, so a child that had not yet appeared never does. Graduated from the example's experimental folder; the gallery's "Morph Component" page is the working use. - The switch's and the slider's thumbs stay out of any
LiquidGlassBatch. Each thumb refracts the track it rides, and a batch's shared copy is taken before the control's own track has painted — batched, the thumb read the page through where the track should have been. Both controls now wrap their thumb inLiquidGlassBatch.excludethemselves, the way the tab bar already keeps its moving pill and its magnifier out, so a switch or a slider dropped into a batch (or into the view's and the scaffold's own) refracts its track exactly as it does outside one. Nothing to pass; the rest of the control is plain paint and never joined a batch to begin with. - The Impeller lens edge is the exact outline again, and the moving-lens fix moved into the frame. 4.1.0 cured the in-flight content wobble by clipping the shader pass to a padded rect on the pixel grid, which also left the outline's outer anti-aliasing half in place — every lens read about half a logical pixel larger and softer at the edge. The clip is the exact outline once more, as in 4.0.0, and the wobble is handled where it comes from instead: the engine rounds the pass's coverage out to whole pixels, so the shader's frame is now packed against that rounded corner — the same treatment an overscroll subpass and a batched pass already get. The moving pills — the tab bar's glass pill and magnifier, the slider's thumb, the switch's thumb — keep the 4.1.0 clip. The slider's and switch's glass is switched off at rest, under the opaque rest pill, so an idle control pushes no backdrop pass at all; the switch's rest pill moved beside its lens for that.
- New —
LiquidGlassBatch: as many lenses as you like, one read of the backdrop. A lens on Impeller works by copying back what is already painted behind it and running the glass shader over that copy, and the copy is the expensive part — it happens per lens, twice per lens once there is blur, since the Gaussian is a backdrop pass of its own stacked under the shader. Wrap a subtree in a batch and everyLiquidGlassLensinside it is tagged with one shared backdrop key, so the engine takes that copy once and all of them sample it. Nothing else about a member changes: its own shape, its own style, its own child, its own adaptivity, its own touch response. That is the whole difference fromLiquidGlassGroup, which fuses its members into a single surface and pays for it with an eight-member ceiling — a batch fuses nothing and has no limit on how many members it holds. Reach for the group when the lenses should read as one piece of glass, and for the batch when there are simply a lot of them: a list of glass cards, a grid, a keyboard, a wall of controls. A batched member with blur folds its Gaussian into that single pass rather than stacking a second one under it — stacked backdrops chain, so a shader sharing the batch's key would read the copy taken before its own blur ever landed and come out unfrosted. The one rule follows from the shared copy: members must not overlap. None of them can see another's glass, so where two overlap the upper one refracts what was behind the lower instead of the lower's surface, and the stack reads as a single lens. Lenses laid out side by side never meet this. Two that must overlap belong in different batches. It needs nothing of the components: every one that draws its glass through aLiquidGlassLens— the button, the FAB, the app bar, the sheet, the text field, the dialog, the tab bar's capsule, the draggable — joins a batch simply by being inside it, since the lens finds the batch from its own context however deep in a component's tree it sits. A component that stacks glass on glass is an overlap like any other and the rule below applies to it: the tab bar's moving pill refracts the capsule under it, so a tab bar put in a batch loses that and the pill reads the page instead — keep it out of the batch, or wrap it inLiquidGlassBatch.exclude, which puts that one subtree back on reads of its own while the rest of the batch stays shared.enabled: falsedoes the same for a whole batch.LiquidGlassGroupandLiquidGlassBlenderjoin a batch too: the fused pass is already a single read, and inside a batch it takes the batch's copy rather than one of its own. So doesLiquidGlassScrollEdge: its blur is a backdrop pass of its own, and in a batch that pass reads the shared copy too. The band is pinned over what scrolls under it, which makes it an overlap in the batch's sense — a card passing through it loses its glass inside the band, small at the band's sigma and gone where the band has faded — so keep the edge out withexcludeif that shows.LiquidGlassViewandLiquidGlassScaffoldnow batch on their own (batch: true, the default): one batch over the background — the scaffold's body — and another over what floats above it. Two rather than one because the chrome refracts the body, which it could not see from the body's copy. A glass pill tab bar keeps its moving pill and its magnifier pill out of both, since each sits on the capsule to refract it; the switch and the slider keep their thumbs out for the same reason.batch: falseinserts no batch at all, so a batch the app put around the view still reaches its lenses. The shared key is an Impeller mechanism; on the Skia capture path aLiquidGlassViewalready captures its background once for every lens inside it, so the batch is inert there rather than wrong, and the same tree runs on both. - New —
LiquidGlassSheetandshowLiquidGlassSheet: Flutter's bottom sheet, in glass. The presenter isshowModalBottomSheet— same route, same slide-up, same drag-to-dismiss, same barrier — with the glass put where its filledMaterialused to be (backgroundColortransparent, no elevation, no clip of its own) and every parameter of Flutter's forwarded:isScrollControlled,scrollControlDisabledMaxHeightRatio,constraints,isDismissible,enableDrag,useSafeArea,useRootNavigator,routeSettings,transitionAnimationController,anchorPoint,barrierColor. Nothing about how a sheet behaves is this package's invention, so it behaves exactly as the one you already use. What is ours is the surface and theLiquidGlassStylethat describes it.LiquidGlassSheetis that surface as a plain widget — the glass, agrabber, a full-widthheader, the padded child — taking the height it is given and hugging its content when given none, so it drops into anything that owns the motion: bottom-align one on a page, or put it insideDraggableScrollableSheet's builder, which is where it has to be for an expandable sheet, since the glass must sit within what resizes.anchorpicks the shape:floatinginsets it and rounds all four corners,attachedruns it full width along the bottom edge — built taller than it is, with the extra hanging off the screen, so the bottom corners are never in frame and no sliver of page shows underneath.avoidKeyboard(on by default) adds the bottomviewInsetspadding you would otherwise write in every builder. On the Skia capture path the sheet refracts the page like the dialog does:showModalBottomSheetalready carries the ambient themes across, and the view's lens scope rides along with them. - Fixed — during an overscroll, a lens inside the scrollable drew its glass away from its own outline, and refracted the rows above and below it (Impeller). For exactly as long as a pull lasts, Flutter's stretch overscroll wraps the whole scrollable in an
ImageFiltered, which renders that subtree into a texture of its own and distorts the texture.ImageFilter.shader'sFlutterFragCoord()is measured from the corner of whatever surface the pass renders into — so inside that texture it stops meaning the window, while the lens went on placing itself in window coordinates. The glass drew offset by the texture's origin (the distance from the top of the window to the top of the scrollable, constant however hard you pulled) while the clip, which rides the layer tree, landed correctly regardless; and because the sampling window was the window's size rather than the texture's, the page was squeezed into the glass and content from outside the lens turned up inside it. Nothing in the lens could see any of this coming: an image filter is not a transform, sogetTransformTo(null)still reported the window position and every transform probe stayed silent — the composited layer is the only tell. A lens now finds the nearest ancestor compositing anImageFilterLayerand measures both its geometry and its sampling window from there, and the tracking layer watches that ancestor appear and disappear so a resting lens still learns the pull began. Both engines are fixed: the layout-drivenLiquidGlassLens, which resolves it at compositing time and so is never a frame behind, and the positioned engine behind the slider and the switch, which picks it up on the post-frame beat it already runs on. A lens with no filtered ancestor — every lens outside this case — packs the same uniforms it always did. One limit stays, because it is the isolation itself and not the geometry: that texture holds only what the scrollable painted, never the page behind it, so for the length of a pull a lens refracts its own list and nothing else, and one with nothing behind it in the list reads black until the spring-back. Dropping the indicator (ScrollConfiguration(behavior: ...copyWith(overscroll: false))) removes the isolated layer altogether, and with it that last caveat.
4.2.0 #
- New —
LiquidGlassAdaptivity: glass that agrees with what is behind it. Give a style anadaptivityand two things flip together, animated overduration: the glass tint, which overridesappearance.color, and the content colour, installed over the child as anIconTheme+DefaultTextStyleso anyIconorTextthat does not hardcode a colour follows on its own. The verdict is the mean perceptual lightness (CIE L*) of the pixels behind that surface, split bydarkBelow/lightAbove— both0.60, so mid-grey is handed to the dark palette rather than divided at the perceptual middle: over a genuinely mid backdrop the smoked palette carries content better than the milky one. Equal thresholds are a plain split; open a gap between them for hysteresis. Nothing samples pixels unless a view opts in —adaptiveSamplingon aLiquidGlassView, oradaptivityon aLiquidGlassScaffold— and one deliberately tiny capture (pixel ratio0.05, 8 per second) serves every adaptive surface in that view, so ten of them cost one capture, not ten.permanentBrightnesspins a surface and samples nothing at all;continuousGlassColorglides the tint through the whole range between the two palettes instead of switching between them, while the content colours keep their binary flip so text never sits at a muddy half-tone;LiquidGlassAdaptivity.noneopts a surface out of whatever it would otherwise inherit. The full guide isADAPTIVITY.md. - New — sharing one verdict:
LiquidGlassAdaptiveArea,LiquidGlassAdaptivityLink,LiquidGlassAdaptivityController,LiquidGlassAdaptiveContent,LiquidGlassAdaptiveSampling. Per-surface verdicts are right until two surfaces sit over different pixels and must still agree — a header and a footer, a bar and a floating button. An area samples one region and hands that verdict to its whole subtree with no wiring; a link is the channel for followers that cannot sit inside it, an area publishing on it and every consumer carrying the same link following; the controller pauses and resumes a whole group, plusadaptOnce()to take a single look on your own cue — a scroll settling, a page entering — rather than sampling throughout.LiquidGlassAdaptiveContentextends the same treatment to bareTextandIconwith no glass behind them, and takes abuilder—(context, color, brightness)— for content that ignoresIconTheme/DefaultTextStyle, like anSvgPicture, or for a halo that has to invert along with the ink it sits under.LiquidGlassAdaptiveSamplingtunes the capture the whole view shares:pixelRatio,frameLimit,minimumRegionSamples. - New — the scaffold adapts as a whole:
LiquidGlassScaffold.adaptivityandLiquidGlassScaffoldAdaptivity. One config is handed down to the app bar, the bottom bar, the action button and anylenses, each of which then judges the background directly behind itself — one config, one verdict per surface, and a surface still escapes with its ownstyle.adaptivityorLiquidGlassAdaptivity.none. It is also the sampler switch: a descendant carrying its own adaptivity cannot open captures on a scaffold that never asked for them. And it drives the OS bars:systemChromedefaults toLiquidGlassSystemChrome.statusBar, pinning an invisible strip along that screen edge which judges that band and annotates the bar's icon brightness — icon brightness only, bar colours are never touched — with.navigationBar,.bothand.nonethe other choices. Because it lives on the config, a scaffold with noadaptivitynever touches the system bars: a strip is only worth pinning where there are pixels to judge it from. A strip never captures the chrome, so the app bar is judged by the pixels behind the app bar and not by the status bar's band; to couple them, publish from an area and point the strip at that link withtopFollowLink/bottomFollowLink, which stops it sampling entirely.topHeight/bottomHeightsize a judging band (the safe-area inset, floored, by default) anddebugBoundsoutlines every strip that was pinned. - New —
LiquidGlassBrightnessFallback: what to guess when there is nothing to sample. With nopermanentBrightness, no link to follow, no enclosing area, no sampler and noinitialBrightness, a surface still has to guess what it is sitting on. The default,appTheme, readsTheme.of(context).brightness: an app that themes itself dark is overwhelmingly likely to be painting dark backgrounds, which makes it the better proxy for a backdrop nobody can read.platformtracksMediaQuery.platformBrightness— the device's dark-mode switch, whichMaterialApp.themeModedoes not affect — for glass that should follow the OS regardless of how the app is themed, and outside aMaterialApp:Themehas nomaybeOf, so with noThemeancestor — a bareWidgetsApp, aCupertinoApp, a widget test without one —Theme.ofsilently returns Flutter's fallback theme andappThemereads light instead of following the device. Only this last resort is involved; every step above it in the chain is untouched, and a surface that can actually sample its backdrop never reaches it. - New —
LiquidGlassGroup: many lenses, one surface. EveryLiquidGlassLensbeneath a group is drawn as one sheet — one backdrop read and one material for the whole set, instead of one of each per lens — which is what makes a row of glass controls cost roughly what a single one costs.smoothnessisnullby default, so the members share the sheet without fusing: the shader skips the smooth-union entirely rather than running it and finding nothing to blend, and members laid out apart — a row of buttons, a column of pills — stop paying for a bridge that never forms. Pass a radius to get the metaball merge, where neighbouring members grow a smooth bridge as they approach and pull apart as they separate. Two to eight members, as withLiquidGlassBlender. - New —
LiquidGlassScrollEdge: the iOS scroll edge treatment. A fading band pinned to a screenedge, there so floating chrome stays legible over whatever scrolls under it.blurdefaults to5, because a dim alone rarely manages that over a busy photograph — passblur: 0and noBackdropFilterexists in the tree at all. The falloff is shaped byblurCurve,Curves.easeInQuartby default: 94% of the sigma is still present at mid-band and 76% at 0.7 before it releases, and about a third survives at 0.9, so the band holds soft through the middle and lands firm. It blurs with exactly oneBackdropFilterat any sigma and on every backend —stylechanges only whether that single pass is feathered (soft) or uniform (hard), never the cost — and the feathering is adstInmask from the inside, so a soft band meets sharp content with no seam. It carries anadaptivitylike every other surface, so the band's own colour flips with the background it fades over. - Fixed — a page with a glass-pill tab bar rendered its BODY text in the wrong font. The scaffold wraps its background in a transparent
Material— which is what installs the theme’sDefaultTextStyle/IconTheme— but only on the plain path; the glass-pill path handedbodyto the bar raw, leaving the whole page under noMaterialat all.MaterialAppinstalls its red/yellow_errorTextStyleapp-wide precisely so that is obvious, but it is only loud when the text sets nothing: aTextthat overrides colour, size, weight and decoration — as any real page does — still inherits that style’sfontFamily: 'monospace', so the page just looked like it had changed font. BareIcons lost their theme the same way. The pill path now wraps the body exactly as the plain path does, so which bar a page uses no longer changes how its text looks.
4.1.1 #
- Fixed — on Impeller, the content seen through a lens wobbled while the lens moved or squashed. A lens on the Impeller path samples the live backdrop through backdrop passes, and the engine places each pass inside its clip's bounds rounded to whole physical pixels, every frame. A moving pill's box lands on a different fraction of a pixel every frame — and a stretch/squash moves each edge independently — so the rounding re-framed the refracted content frame to frame, while a still lens was rock solid. The shader pass no longer clips to the outline: it clips to a rect padded past the lens box and snapped outward to whole physical pixels, so its bounds only ever step by exact texels. The outline itself is still drawn by the shader from the fractional geometry, so a squash animates as smoothly as it always did, and the blur keeps its exact outline clip (blur fills whatever clip it is given). Nothing changes on the Skia path. One visible consequence: the glass's edge now keeps the outer half of its anti-aliasing ramp that the outline clip used to trim — about half a logical pixel softer.
- Fixed — the slider's rest pill showed a hair of glass around it on Impeller.
LiquidGlassMotionPilldrew itscover(the slider's white rest pill) as the lens's child, clipped to the lens box — so once the glass's edge reached past the outline (above), a thin ring of refracted content peeked out around the rest pill. The cover is now its own layer above the lens, sized to the glass's visible extent — the deformed box plus that half-pixel reach on the Impeller backdrop path, nothing extra on Skia — and clipped to the matching outline with the live stretch, so at rest it hides the glass completely and still deforms as one body with it. Only hosts that pass acoverare affected; in the package that is the slider.
4.1.0 #
- New —
LiquidGlassFab: the floating action button, in glass. A circular button built from aniconor anychild, plusLiquidGlassFab.extended(...)for the icon-and-label pill, carrying the Material fields you would reach for —heroTag,tooltip,foregroundColor,iconSize,padding— and one Material has no need of:visibility, which animates the glass away instead of dropping the widget out of the tree. The look ships tuned asLiquidGlassFab.defaultStyle(a faint white tint, a small blur, gentle refraction) with its contact shadow ridingappearance.shadow, like every other lens's; astyleyou pass is merged over that default, so one facet changes without retyping the rest, and an appearance carrying no shadow drops the shadow. - New — glass dialogs:
showLiquidGlassDialog,LiquidGlassAlertDialog,LiquidGlassDialog. The presenter pushes throughshowGeneralDialogand carries the ambient themes across withInheritedTheme.capture;LiquidGlassAlertDialoglays the familiar icon / title / content / actions rows onto a glass card (every padding, text style and overflow knob of Material's exposed), andLiquidGlassDialogis the bare card for anything else. Both shareLiquidGlassDialog.defaultStyleand merge over it. The entrance is scale only — deliberately no fade: anOpacitylayer isolates a lens's backdrop, so a fading dialog loses the page behind it mid-flight (no frost, dark buttons) and snaps in when the layer drops. The barrier fades on its own and the glass condenses over it, live the whole way. - New — the scaffold has a
floatingActionButtonslot, and it clears the bar it can see. The button sits in the lens layer with the other slots, so its glass refracts the body on both backends.floatingActionButtonAlignmentdefaults toAlignmentDirectional.bottomEnd— bottom-right in LTR, bottom-left in RTL — and takes any alignment, physical or fractional. When the button is aligned toward the bottom the scaffold measures its ownLiquidGlassTabBarand lifts the button clear of it; a bar it cannot measure from there falls back to a fixed clearance, andfloatingActionButtonClearanceoverrides either. With the default alignment it shares the corner withbottomNavigationBarAction, so use both only when a bar sits between them. - New —
LiquidGlassScaffold.dialog: a glass panel that never leaves the view.showLiquidGlassDialogpushes a route; this is the other answer — an ordinary widget in the scaffold's lens layer, so it refracts the livebodyon every backend and aLiquidGlassBlendercan merge it with the other slots' glass. What it trades away is the navigator: no route, noFutureto await, so the page owns the open state and clears the slot to close (onDialogDismissedfires on a barrier tap or a system back gesture). The layer keeps painting through the exit and only then lets the panel go, and the scrim fades on its own —dialogBarrierColor,dialogBarrierDismissibleanddialogTransitionDurationtune the rest. - Fixed — a dialog's glass came out flat on the Skia capture path. A dialog builds in the navigator's overlay, not inside the
LiquidGlassViewthat captured the page, so its lens had nothing to sample and fell back to frost. The view now plants a portal around its whole subtree that rides along withInheritedTheme.capture— the same tripThemealready makes — and unpacks a real scope inside the route, so a dialog opened from a context inside the view refracts the page behind it. The portal is not a scope itself, so a lens down in the captured background still finds nothing, as it must: it would be refracting a picture of itself. Impeller never cared — it was sampling the live backdrop all along. - Fixed — a lens under an animated ancestor transform drew its glass for the untransformed rect (Impeller). The Impeller shader paints in screen space from uniforms — top-left via
localToGlobal, size from layout — while the clip is a widget that transforms with the tree, so aScaleTransitionover a lens (the dialog entrance) scaled the clip but not the shader's geometry: the border, the outermost band of what the shader draws, fell outside the smaller clip and was cut until the scale reached1.0. The lens now reads its full global transform (the tracking layer already watched it) and, when an ancestor carries real scale or rotation, hands the shader a lens→shader affine map (u_xformRow/u_xformOff, main shader only): the fragment maps through the inverse so ALL geometry — outline, corners, band, rim — evaluates in lens space, and the refracted position maps forward again so the backdrop is sampled in screen space. Scale, rotation, even mirroring now land the glass exactly on the widget clip at every frame. Translation-only lenses — every lens that merely sits somewhere — keep the legacy screen-space uniforms bit-identical, and the Skia path (already local-space by construction) passes identity. One property to know: the border's light direction now rides the lens, so a rotated lens rotates its glint with it.
4.0.0 #
- Breaking — the navigation components take their real names:
LiquidGlassTabBarandLiquidGlassSwitch. The bottom nav bar is what iOS calls a tab bar, and the package now calls it that:LiquidGlassBottomNavBar→LiquidGlassTabBar(including.withImpeller), with the style groups cascading —LiquidGlassNavItemStyle→LiquidGlassTabItemStyle,LiquidGlassNavPillStyle→LiquidGlassTabPillStyle,LiquidGlassNavMagnifierPillStyle→LiquidGlassTabMagnifierPillStyle,LiquidGlassBottomNavBarLayout→LiquidGlassTabBarLayout. The old floating 2–5-tab capsule that held theLiquidGlassTabBarname is deleted (with its never-exported animated variant): the bar covers its use cases and places anywhere viaalignment/margin. The toggle takes the name the ecosystem uses —Switchin Flutter,UISwitchin UIKit:LiquidGlassToggle→LiquidGlassSwitch(withLiquidGlassSwitchLayout). Migration is a find-and-replace; no parameter changed meaning, andLiquidGlassTabBarItem/LiquidGlassTabBarActionalready carried the tab-bar name. Entries below use the new names throughout. - Breaking — the
.customconstructors are gone; custom content moves to the main constructors..customexisted only to relax onerequiredparameter without breaking the API; with this cycle already breaking, the parameter is simply relaxed at the source instead.LiquidGlassButton.label,LiquidGlassTabBarAction.icon,LiquidGlassTabBarItem.icon(and the same on the internal dock/app-icon/control-tile) are now optional, each guarded by a debug assert that the component was given something to show — a label, an icon, a builder, or achild. Migration is mechanical: delete the.customsuffix (LiquidGlassButton.custom(child: …)→LiquidGlassButton(child: …),LiquidGlassTabBarItem.custom(iconBuilder: …)→LiquidGlassTabBarItem(iconBuilder: …)); every parameter keeps its name and meaning. The never-renderedkLiquidGlassCustomGlyphplaceholder dies with them. The item deliberately keeps builders rather than gaining achild— the reveal tiers render each tab twice per frame, which is precisely what a builder can do and one widget cannot. - Breaking — the jelly family is removed; the spring integrator survives. Nothing in the package ran on it anymore: the slider thumb, the switch and the tab bar's moving pill all deform through the acceleration model (
LiquidGlassLensMotionSpec), and the flex touch response has its own springs. Gone: theLiquidGlassJellywidget,LiquidGlassJellyConfig/LiquidGlassJellyStyle, the drag-drivenLiquidGlassJellySpringsimulation, and the internal (never exported)LiquidGlassPillJellydescriptor and jelly resolver. The one thing all of them shared — the 240 Hz sub-stepped underdamped spring step — is exactly what stays:liquidGlassSpringStep, now inliquid_glass_spring.dart, still public and still the integrator every component steps its springs through. - Changed —
LiquidGlassSliderandLiquidGlassSwitchship the tuned look instead of asking for it. Both controls looked unfinished out of the box: the thumb wore a milky tinted glass with a real1.5 pxblur, and it cast nothing, so it read as painted onto the track rather than lying in it. Every showcase therefore restated the same style by hand. That style is now the default, exposed asLiquidGlassSlider.defaultStyleandLiquidGlassSwitch.defaultStyleso one facet can be changed without retyping the rest (defaultStyle.copyWith(refraction: …)). It is a clear pill — refraction and a soft rim, no tint — with the blur down to0.5, since at thumb size a real blur turns the magnified content to mush while the refraction reads sharper without one. Both also default to a tucked-in contact shadow (LiquidGlassShadow(inset: 3), the glass overhanging its own shadow, since at this size a full-width halo reads as a glow rather than contact) — and it rides in the style, ondefaultStyle.appearance.shadow, rather than as a parameter beside it: a contact shadow is part of how a surface looks, so it travels with the rest of the look wherever the style goes. Retune it withdefaultStyle.copyWith(appearance: defaultStyle.appearance.copyWith(shadow: …)), or hand over an appearance carrying none to drop it. Neither control takes ashadow:parameter. Two numbers move with them: the slider'sinactiveColorgoes from white at 23 % to black at 8 %, which is the value that reads on a light page — over a dark one, pass a translucent white as before — andLiquidGlassSwitchLayout.pinchedHeightgoes from20to22, a shade less pinch in the slice behind the glass. Anything passing its ownstyle/inactiveColor/layoutis unaffected. - New —
widthandheightonLiquidGlassSliderandLiquidGlassSwitch. Both sizes were reachable only through the layout descriptor (layout: LiquidGlassSliderLayout(width: 320)), which is the first thing anyone wants to set and the last place they look for it — the components size themselves from their own layout, so wrapping one in aSizedBoxdoes nothing and there was no visible way in. Both controls now takewidthandheightdirectly, as a shorthand for the two same-named layout fields; they win over the layout when both are given, andnull(the default) leaves it in charge, so nothing existing renders differently. The layout keeps everything else — the thumb pair, the track thickness, the icons, the pinch — and stays the way to state the whole geometry at once. Two boundaries worth knowing, both documented on the parameters: on the slider,heightis vertical room (the lifted thumb overhangs the track and grows further as it squashes, and all of it has to fit inside the glass capture) rather than thumb size, and leaving itnullderives the safe value; on the switch, the two size the track, not the thumb, so a switch that should grow in proportion wantsLiquidGlassSwitchLayout().scaled(…)instead. - New — a contact shadow that belongs to the material:
LiquidGlassAppearance.shadow. The appearance group now carries an optionalLiquidGlassShadowdescriptor, honored byLiquidGlassLens: the lens wraps itself in the ring it describes, corner defaulting to the lens shape's own radius, visibility composing with the lens'svisibility. Because every component builds its surface from an appearance, the shadow rides along wherever a style goes. The wrap sits inside the flex deformation's box, so on a lens withtouch:the ring is not frozen on the rest silhouette — press and it swells, leans and springs back with the body, exactly as the glass does, at no extra cost on an untouched lens.LiquidGlassShadowstays a standalone component, unchanged, for wrapping anything by hand. Two boundaries: a shadow is a descriptor, not an animation, so the moving pill's material lerp carries the nearer end's shadow rather than blending two rings; and appearance shadows are not supported inside aLiquidGlassBlender— a merged metaball silhouette has no single ring to cast. The tab bar authors both of its shadows here too — the bar capsule's on itsstyle's appearance (see the capsule entry below), the moving pill's on its glass style'sappearance.shadow; neither is a flat parameter, and the bar hoists the pill's descriptor into its own tracked ring (scaled with the outline, faded with the rim), which a static wrap could not do. - Changed — the glass pill ships its tuned look, and its shadow lives in the appearance.
LiquidGlassTabPillStyle's defaults are now the flagship look instead of a placeholder: the pill is pure refraction — no tint of its own, since over glass a fill only flattens the capsule — through a thin-rimmed continuous capsule (LiquidGlassShape(borderWidth: 0.5)), withdistortion0.06 → 0.04,distortionWidth10 → 12,growHeight12 → 9, the rest fillcolora barely-there grey (0x2EAEAEB2, was0x26FFFFFF), and a default contact shadow — a soft ring spreading past the rim (LiquidGlassShadow(blur: 9, opacity: 0.3), no inset tuck) riding the default glass appearance. The pill's shadow is authored where every lens's is: on the glass style'sappearance.shadow— there is noshadow:parameter beside the style; aglassStylewhose appearance carries its own shadow replaces the default, and one carrying none drops it. The bar hoists the descriptor into the pill's own tracked ring rather than letting the inner lens wrap a frozen one, so the ring still rides the squash and fades with the rim. And the tier itself is no longer an opt-in:modenow defaults toLiquidGlassPillMode.both— the glass-refracting pill on every renderer, which the sleep-at-rest work makes an honest default (a settled bar costs no shader pass, no clip and no capture on either backend; Skia pays only during the travel itself).LiquidGlassPillMode.impellerOnlyremains the degrade-gracefully choice for weak Skia/Web targets, andnonethe fully flat tier. - Changed — the switch's thumb is a
LiquidGlassLens, and its contact shadow is an appearance shadow. The thumb was the last mainline glass on the classic position-driven engine — abuildLiquidGlassMorphPillconfig in the switch's own view. It is now a layout-drivenLiquidGlassLensin that view's child slot, at the same per-frame box the pixel-snap work already agrees on with the hole and the rest pill, resolving its material through the same logic the config used — extracted asresolveLiquidGlassMorphPillStyle, whichbuildLiquidGlassMorphPillitself now calls, so the two can never drift. Nothing about the feel changes: the morph springs, the pinch, the hole, the gesture, the rest pill riding inside the lens are all untouched. The thumb's shadow moves from a hand-placed sibling in the view's child slot to the lens's ownappearance.shadow, still faded with the morph so only the glass pill ever casts one, still painting behind the glass and above the captured track. With this, every component's mainline glass goes throughLiquidGlassLens; the classic engine remains only inside the experimental copies. - Changed — the tab bar's capsule is a
LiquidGlassLenson every tier, and the bar shadow is an appearance shadow. The glass-pill tier's capsule was the last piece of the bar on the classic position-driven engine — aLiquidGlassconfig in the inner view'schildren:pipeline. It is now a layout-drivenLiquidGlassLensin that view's child slot (Skia refracts the view's captured background, Impeller the live backdrop, exactly as before), positioned off the same_barLeft/_effBottomMarginthe icon shell, the gesture overlay and the moving pill already read — one source of truth, so the four can never disagree by a pixel. With the capsule a real lens, the bar's shadow stops being a hand-placed sibling widget and ridesappearance.shadowon both tiers — authored on the barstyle's appearance, with no flat parameter beside it: the lens wraps itself in the ring, behind the glass, inside the outer view's capture — so the moving pill still refracts the bar's own shadow. One visible nuance: the ring now paints under the capsule's glass rather than over it (the plain tier always did it this way; the tiers now agree). Impeller's lenses are stackedBackdropFilters, and each samples only what is painted beneath it — so the bar now mounts a second, invisible magnifier pill inside the inner stack, above the bar capsule but below the icon shell: the same silhouette, lift, travel, squash and shed as the glass pill, mounted and retired in the same frame, but fully transparent, unblurred, undistorted, rimless and shadowless. Its backdrop is the page and the capsule alone, so its one job — magnification, ramping in with the lift and out through the handover on the glass material's own curve — pushes the bar back under the pill without shrinking the icons drawn above it. The glass pill on top then refracts the receded bar and the crisp icons alike, its own magnification held at1so the two never compound. Configured byLiquidGlassTabPillStyle.magnifierPill, aLiquidGlassTabMagnifierPillStylewithenabled(a feature switch, not an animated visibility — mounted, it already appears and retires with the glass pill) andmagnification(default0.87). The Skia capture path cannot chain lenses and is untouched — there the single pill keeps its configured magnification exactly as before. A settled bar still mounts neither pill. - New — the tab under the glass can be bigger than the rest, and "under glass" is its own state. A tab used to be selected or not; it is now one of three: unselected, selected (the committed tab — its color, weight and
selectedIconart, whether or not a pill is on it), and under glass — the moving glass pill is physically over it right now, lifted on it, dragged across it, or sweeping past it mid-travel.LiquidGlassTabItemStyle.underGlassIconSizeandunderGlassLabelFontSizesize the icon and label for that third state only, and the enlargement is the glass's effect, so it lives and dies with the glass rather than with the selection: it rides under the pill for the whole travel, and on landing it is not cut off at the swap to the static rest pill — the sizes lerp on the same shed signal that retires the rim and the refraction, so the icon glides back down to the sharediconSizethrough the handover and the swap happens on two identical pictures. A flat pill is not glass: the static rest highlight, the non-glass tiers' pills and a hidden pill (show: false) never enlarge anything — a selected tab under them shows its selection through color and weight alone. Both sizes default tonull(no change), so existing bars render unchanged, and custom items see the same state —LiquidGlassGlyphandLiquidGlassLabelnow carry anunderGlassflag alongsideselected. One property of the reveal to know: mid-travel the layer inside the pill draws at the under-glass size while the layer outside stays at the shared size, so the clip edge joins two sizes — a modest delta reads as the glass magnifying the icon, a large one reads as a seam. - New — custom tab labels (
LiquidGlassTabBarItem.labelBuilder). The label was locked to a plainTextbuilt from the item'slabelstring, so a custom font, rich text or a badge row could not be used at all — andLiquidGlassTabBarItem.customcould restyle its icon but never its label. An item can now draw its own:labelBuilderis handed aLiquidGlassLabelcarrying the already resolved color, font size and weight for the layer being drawn (bundled aslabel.textStyle, socopyWithkeeps the stock look and changes only what you need) plus the item'slabeltext and the layer's selected state. LikeiconBuilder, it runs once per rendered layer, so custom labels get the moving pill's reveal instead of sitting frozen while it sweeps past; return the same-sized widget for both states or the reveal clip will show a seam. Honored by every tier through one shared build helper, andlabelmay be omitted entirely for builder-only content. - Fixed — a travelling pill deformed one way on the launch and the opposite way on the landing. The acceleration model is signed on the force, so which way the pill deformed depended on which way it was going: a tap to the right stretched it wide as it launched and squashed it narrow as it braked, and a tap to the left did both in reverse. Every journey therefore contradicted itself halfway through, and the half still on screen when you looked at the tab you had chosen was the one you never chose. The travel now keeps the force's magnitude — the launch peak, the lull at constant speed, the braking peak, all unchanged — and takes its sign from the direction of travel instead, so the deformation the pill lands on is the one it carried the whole way: a tap to the left stretches it wide, a tap to the right holds it narrow and tall. The landing itself is untouched, to the digit; only the launch changed, and it changed to agree. A finger is deliberately left on the raw force, where pushing right stretches and pushing left squashes — a drag is something you are doing to the pill, and it should answer the hand. Tapping a tab on the far side mid-flight crosses between the two keys rather than switching, so a reversal passes through undeformed instead of flipping the pill inside out in one frame.
- Changed — the glass pill now holds its raised size for the whole travel instead of bulging through it. Tapping a tab used to grow the pill along a sine envelope: biggest at the halfway mark, back to its resting size exactly as it arrived, so the size peaked where nothing was happening and was already gone by the time you looked at the tab you had chosen. The lift is now a held state. It goes up the instant a tab is tapped, stays up for the entire journey, and is only released once the pill has landed — so the pill arrives at full size and deflates where it stands. A press-and-hold is the same state held open by a finger: letting go starts the snap, not the settle, and the pill comes down when it gets there rather than the moment it is released (the old
_settleGrowshrink is gone, and the two gestures now run the same path). Three springs carry it, all under or at critical damping: one per axis for the size, at damping ratios that differ just enough that the width overshoots a shade further and settles a shade later than the height — so each end of the lift overshoots, rebounds, and dips a little under the resting size on the way back — and a third, much stiffer and critically damped, for the material, so the glass is fully on long before the size stops moving. The acceleration squash/stretch is untouched and multiplies on top, which puts the braking squash and the deflation in the same moment.handoverStartis now that one landing gate — the lift release and the glass shed both hang off it — and its default moves from0.72to0.92, the last of the travel where the spring is creeping the final pixels in. - Changed — the glass pill now turns into the resting pill instead of being swapped for it. From the landing (
handoverStart) the lens sheds its glass while everything else keeps running: the rim and its contact shadow go first, then the refraction band narrows to nothing behind them. The deflation and the acceleration squash are untouched throughout — the pill is still shrinking and still deforming while it stops looking like glass. Only once there is nothing left in it to see is the lens dropped and the plain pill put in its place, and by then the lens is drawing a flat fill at rest size, which is exactly what the plain pill draws. A settled bar still costs no shader pass, no clip and no outer capture. The shed is a target rather than a switch, so tapping another tab midway turns the glass back around (glassReturnTau) instead of finishing and starting over. - Fixed — the pill stopped deforming halfway through every travel. The acceleration squash was cut at the 50% mark and the returning half ran on the size envelope alone, which threw away the braking squash — the landing — that the model exists to produce. The model also lived inside the glass widget, so it could not have outlived it. It now lives on the bar: the deformation belongs to the selection pill as a thing rather than to whichever widget is drawing it, it runs unbroken for the whole travel, and the bar can see when it has drained and only then retire the lens.
- Fixed — the moving glass pill popped as it handed over to the resting pill. The travel envelope already returned the pill to its rest material exactly as the swap fired, but that rest material was not glass-free:
LiquidGlassTabPillStyle.effectiveRestnever set a refraction, so it inheritedLiquidGlassStyle's full-strength default (distortion0.1over a 30 px band) — stronger than most lifted pills, which inverted the interpolation across the whole travel and left the pill still bending the bar at the instant a non-refracting painted pill replaced it. The rest end is now explicitly inert, the moving pill also retires the rim (borderWidth) at that end since it comes from the host's authored corner shape and cannot interpolate itself away, and aLiquidGlassRefractionTypenamed only on the lifted style now fades by width instead of surviving at full strength through the rest state. The lifted look is unchanged. - Changed — the bottom nav bar's glass pill now deforms from acceleration, not from a jelly spring. The pill's drawn position is sampled every frame in pixels, differentiated twice, and the averaged acceleration scales it oppositely on the two axes (
scaleX = 1 + d,scaleY = 1 − d): it stretches wide and flat as it launches off a tab, squashes narrow and tall as it brakes into the next, and sits undeformed at constant speed. Force, not speed — and there is no lean term, so the pill deforms about its centre and travels on the spring alone. Because the model reads the position the pill is actually drawn at, a drag-release snap feeds it like any other motion and its braking IS the landing squash, with no special-casing. The pill is now a lens widget in the outer view'schild:slot, so its outline stretches as one body (elliptical end caps) instead of the capsule being re-rounded at each new size; one consequence is that the pill paints below the outer lenses rather than above them. It is also mounted only while something moves it — at rest the bar drops to a plain fill with no shader pass, no clip and no outer capture. - Breaking —
LiquidGlassTabPillStyle.jellyis replaced bymotion. The field now takes aLiquidGlassLensMotionSpec(sampleWindow,sensitivity,maxDeformation,responseTime); the default caps the deformation at ±12 %, since the pill travels inside the bar capsule. Code passingjelly:to the nav pill style no longer compiles. (The rest of the jelly family follows it out this same cycle — see the jelly-family entry above.) - New — contact shadows on the nav bar. A shadow authored on the bar
style'sappearance.shadowdraws around the bar capsule; it lands inside the moving pill's capture, so the pill refracts the bar's own shadow. One on the pill's glass style'sappearance.shadowdraws around the moving pill — wrapped outside its lens, so the arc pooling below it is not clipped at the outline, and tracking the live outline stretch so the ring stays on the rim while the pill squashes. The capsule shadow is honored by every tier — the glass-pill bar draws it into the inner stack (inside the pill's capture), the plain bar wraps the capsule lens with it — and followsvisibility.
3.5.0 #
- New —
LiquidGlassTouch/LiquidGlassFlex: a surface that deforms under a finger. Glass could move and it could re-tint, but it could not answer a press with its own geometry. The newtouch:parameter — onLiquidGlassLens,LiquidGlassButton,LiquidGlassAppBarandLiquidGlassTabBarAction— takes aLiquidGlassTouchcarrying aLiquidGlassFlex, and the surface becomes a soft body: press and it swells under the finger, drag and it elongates along the pull, pinches in the cross axis, leans after the finger, then springs back with a wobble on release. The lens itself never moves — its footprint in layout is unchanged, so neighbouring widgets never shift as it wobbles. Unlike a scale transform, which can only grow symmetrically around one anchor, the left/right/top/bottom edges spring independently toward their own targets, so the half nearest your finger deforms more than the far half.nullis the default and adds nothing to the tree: no gesture listener, no ticker, no cost on an untouched lens. The bars (LiquidGlassTabBar,LiquidGlassBottomNavBar) deliberately do not take it — a navigation bar that deforms under a stray drag reads as broken rather than as soft — and neither doLiquidGlassSlider/LiquidGlassToggle, whose touch response is already the jelly. - Nine direct knobs, five behind
LiquidGlassFlexAdvanced:stretch(peak elongation in logical px),squeeze(the along-axis gain is taken back out of the cross axis — elongate horizontally and the lens genuinely gets thinner),lean(how far the body slides after the finger),grip(0= every edge shares the deformation wherever you touched,1= the edges nearest your finger take all of it),compressInward(driving the grabbed edge into the body squashes it instead of inflating it whichever way it is shoved),holdScale(signed swell for as long as the finger is down),tapScale(a one-shot pop fired when a tap completes, so a fast click still reads as a click),maxPull(thetanhsaturation point) andlockAxis(pins the cross axis for shapes whose proportions carry meaning — a nav capsule, a slider track). Content follow, the optical boosts and the three spring constants live inadvanced..subtle(),.uniform()and.pronounced()are tuned presets. - The content rides the glass.
childFollowblends the child's transform toward the lens's own map, and at1the two are literally the same expression — so the content cannot drift off the surface however hard the lens is pulled. The child is laid out at rest size and then transformed, so text never re-wraps and rows never re-space mid-gesture. - A press deepens the optics rather than zooming the backdrop.
refractionBoostscales the refraction itself (through a configuredLiquidGlassRefractionType's own strength when one is set, so a type is not bypassed). The backdrop zoom is a separate knob,magnificationBoost, off by default — magnification is not a refraction depth, and raising it reads as the picture behind the glass sliding rather than the glass bending. - Correct outlines under deformation. The shader evaluates the shape at rest size in a domain divided by the deformation scale, and the clips are stretched to match — so a stretched circle becomes an ellipse instead of a stadium with flat runs, and the clip never crosses the glass and shaves the rim at a cap apex.
- Works inside
LiquidGlassBlender. A deformed member resizes its own box, the blender derives every member rect from the render tree, and the merged metaball silhouette picks the deformation up on its own. The one thing that does not survive the merge isrefractionBoost: the blender refracts the whole surface through a single shared style, so a press on one blob cannot deepen its own optics without deepening every other blob's. All geometry —stretch,squeeze,lean,grip,holdScale,tapScale— pluschildFollowbehave normally. - One finger owns a lens. The driver holds a single grab point and a single accumulated pull, and gestures arrive through a translucent
Listener(never aGestureDetector, so a press can never join the gesture arena and steal taps from the content inside the lens or fight a scrollable beneath it). Pointer events are therefore filtered by identity: the first finger down claims the lens and keeps it until it lifts, a second touch cannot reset the grab point mid-gesture, and a non-owner lifting does not end the press. - Fix —
flutter build webfails withUndefined name 'internal'(reported on Flutter 3.29 / Dart 3.7.2, dart2js):liquid_glass_view.dartandliquid_glass_blender.dartannotate members with@internalbut did not importpackage:meta/meta.dart.package:flutter/foundation.dartonly re-exportsinternalon newer SDKs, so on 3.29 the name resolved to nothing and the compile aborted — for every consumer of the package, whether or not they used the annotated APIs, since the whole library is compiled either way. Both files now import meta directly, carrying an// ignore: unnecessary_importbecause newer SDKs correctly flag the import as redundant.metawas already a declared dependency; nothing else changes. - Example: new glass list page (a single lens holding a four-row list, with every flex value on sliders) and a standalone flex tuner that emits the tuned spec as copy-pasteable Dart.
3.4.0 #
- New — SVG / PNG / any-widget icons in the nav and tab bars (
LiquidGlassTabBarItem.iconBuilder): tab glyphs were locked toIconData, so an SVG or a bitmap could not be used at all. An item can now draw itself:iconBuilderis handed aLiquidGlassGlyphcarrying the already resolved color for the layer being drawn, the glyph box size, and whether that layer renders the selected state — so tinting withglyph.colorand switching artwork onglyph.selectedreproduces exactly whaticon+selectedIcondo. This matters on the glass-pill bar, which draws every tab twice per frame (one copy clipped inside the moving pill, one outside it): the builder runs once per layer with that layer's color, so custom artwork gets the same iOS-26 reveal as a built-in icon instead of sitting frozen while the pill sweeps past. Honored by every tier — the plain bar, the sliding bar, the glass-pill bar andLiquidGlassTabBar. UseLiquidGlassTabBarItem.custom(...)for asset-only tabs (noIconDataneeded), and mix builder items andIconDataitems freely in one bar. Custom content is boxed to the item style'siconSizeand scaled down if larger, so a glyph can never grow its cell and desync the pill geometry from the icon it reveals. No new package dependency — bring your own SVG/image library. - New —
LiquidGlassButton.childandLiquidGlassTabBarAction.child: both components hardcoded their content (an icon + label row; a single glyph), so anything else — an SVG, an image, a badge, two lines of text — was impossible.childreplaces that content entirely, withLiquidGlassButton.custom(...)/LiquidGlassTabBarAction.custom(...)for the content-only form. The widget is centered and clipped to the glass shape and never sizes its host: the button stays on itswidth/heightand the action stays asize-diameter circle, so the lens geometry holds. A bareIcon/Textinside inherits the host'sforegroundColor(and the button'sfontSize/fontWeight/iconSize), so it matches the built-in look by default; give the widget its own color to paint it yourself. - Additive release: no field types changed and no new required parameters, so existing code compiles and renders identically.
3.3.1 #
- Fix —
flutter build webfails on Flutter 3.44+ (impellerc failure: Compilation failed for target: SkSL): Flutter 3.44 started validating every shader in the package manifest against SkSL at build time, and the Impeller-only entries use the hardware-derivative gradient (dFdx(float)) — invalid SkSL — so the web build died even though those entries are never loaded on web. The derivative path is now guarded behind impellerc'sSKIA_GRAPHICS_BACKENDdefine: when a shader is compiled for the SkSL target it falls back to the analytic (derivative-free) gradient, so all entries compile clean. Native Impeller output is byte-identical — real devices keep the cheap 1-tap derivative path — and the runtime per-backend shader selection is unchanged.
3.3.0 #
- New —
OpticalBorder.lightSpread: controls how far the optical rim's directional highlight wraps around the perimeter, independent of the always-on ambient ring.0.0= tight, concentrated glint hugging the light axis,0.5= default (identical to the previous fixed falloff, so existing code renders unchanged),1.0= highlight wraps almost all the way around. Below0.5the angular window of each light lobe narrows — the bright core itself tightens, not just the dim tail. Applies toLiquidGlassLens, the border-only painter, and theLiquidGlassBlendermetaball rim.
3.2.2 #
- Fix — vertically mirrored refraction on OpenGL ES (emulators): Flutter's OpenGLES coordinate-system unification (engine after 3.44.0) removed the framebuffer Y-flip, which turned the shaders' manual sampler/derivative Y-flips into a double flip — a symmetrical Y-axis refraction artifact visible on Android emulators that fall back to OpenGL ES (real Vulkan/Metal devices were never affected). All four
IMPELLER_TARGET_OPENGLESflips are now guarded by#ifndef IMPELLER_OPENGLES_UNFLIPPED_DEPRECATED, so they apply only on older engines and are skipped once the engine sets the macro. Skia/web and Vulkan/Metal are unchanged.
3.2.1 #
- Fix — bottom nav bar tab selection on web: clicking a tab on web could select the wrong tab (the neighbour), walk toward the target one click at a time, or not change selection at all — most noticeable with a trackpad. A quick click was being routed through the hold-to-drag path and committing to the glide's lagging position instead of the cell that was pressed. A click now commits to the pressed tab. The tap-and-hold drag gesture is unchanged.
3.2.0 #
- Per-backend single-lens shaders: the shape-gradient method now differs by engine. Impeller keeps the hardware-derivative (
dFdx) gradient, while Skia/web loads a separateliquid_glass_skia.frag/liquid_glass_border_skia.fragthat selects an analytic gradient instead —dFdxis invalid SkSL, so the previous shared program was only correct on Impeller. Programs are cached per backend and resolved automatically; the frosted fallback now also covers the brief async load when a lens mounts on a backend that wasn't preloaded. LiquidGlassShadersAPI (backward-compatible):ensureLoaded,isLoaded/isLoadedFor,createMainShader, andcreateBorderShadernow accept an optionalimpellerflag, defaulting to the engine's native backend (ui.ImageFilter.isShaderFilterSupported). Existing no-argument calls keep working.- Metaball blend on Skia — quality + blur: the merged smooth-union field now propagates an analytic gradient (the h-weighted blend of the per-lens gradients) on Skia instead of the heavier 5-tap, and in-shader blur is re-enabled on the Skia blend path. Chromatic aberration is applied per blur tap, so CA lands before the blur (matching Impeller's order).
- Blender Impeller perf — tight engine-blur clip: on the Impeller engine-blur path the costly backdrop + blur pass is now clipped to the tight glass region (the blob union plus rim/blur/refraction/bridge margin) instead of the whole surface. The shader is packed in that clip-local frame — lens centres, resolution, and the sampling window all share one coordinate space — so the blob stays locked to the lenses while the expensive pass only covers the glass.
- New —
LiquidGlassBlender.debugClipBounds(off by default): draws a magenta outline around the backdrop clip region on both backends, to verify the costly pass hugs the merged glass. Diagnostic only — it costs a stroked draw every frame; keep itfalsein production. - Example: added a liquid-glass showcase/menu page and an image-backed lens demo to the gallery.
3.1.0 #
- New —
LiquidGlassBlender: blend two to sixLiquidGlassLensdescendants into one liquid surface. Neighbouring lenses fuse with a smooth metaball bridge as they meet and pull apart as they separate, while each member keeps its own corner style. Place it inside aLiquidGlassViewand it works on both backends — Impeller samples the live backdrop, Skia refracts the captured background. - Metaball gradient split: the merged-field normal uses the hardware-derivative 1-tap on Impeller and a 5-tap central difference on Skia (whose SkSL has no
dFdx), selected per backend so the blend loads and renders on both. - Single-lens shaders now use the exact 5-tap gradient for every corner style.
- Chromatic aberration on the Skia blend path is now applied after the blur (matching Impeller's order); in-shader blur is disabled on the Skia blend path for now.
- Note: the blend is not optimized for Skia yet — it works on the Skia capture path but can be heavy there; best performance is on Impeller for now.
3.0.0 #
- New — lens anywhere:
LiquidGlassLensis a layout-driven lens you can drop anywhere in the widget tree (no position/size params; size comes from layout). It supports both Impeller and Skia automatically, resolving the best render path for the running engine: on Impeller it refracts the live backdrop with noLiquidGlassViewand no background widget at all; on Skia it refracts an ancestorLiquidGlassView's captured background, and gracefully degrades to a frosted look when neither is available. - New:
LiquidGlassStyle— one shared descriptor (shape + appearance + refraction) for every glass surface (lens, components, nav pill), withcopyWith(...)andmerge(...). - New — jelly motion: the
LiquidGlassSlider,LiquidGlassToggle, andLiquidGlassBottomNavBarnow share an iOS-style jelly spring — squash/stretch with a direction-memory spring and momentum-sided squash. The slider/toggle thumb stretches toward its travel and snaps back, and the nav bar's selection pill springs between items. Exposed standalone as the reusableLiquidGlassJellywidget. - New components:
LiquidGlassSlider,LiquidGlassToggle,LiquidGlassAppBar,LiquidGlassScaffold(owns the glass pipeline),LiquidGlassDraggable, andLiquidGlassJellyjoin the existingLiquidGlassButton,LiquidGlassBottomNavBar, andLiquidGlassTabBar. - Migration: the old position-driven lens API (
LiquidGlass) is superseded byLiquidGlassLensand the drop-in components; write new code againstLiquidGlassLens. - Breaking: removed the
LiquidGlassSearchBarcomponent. - Breaking:
LiquidGlassAppIconandLiquidGlassDockare no longer part of the public API (kept internally for maintenance). - Rewrote the README around the lens-anywhere API and consolidated the example into a single
main.dartgallery whose home menu opens each demo as its own route. - New shape:
LiquidGlassShape.continuousRoundedRectangle(...)— an Apple capsule-style continuous rounded rectangle, now the default shape; it collapses to a clean capsule at full corner radius. - Breaking: removed
SuperellipseShape; its L^n squircle now lives onLiquidGlassShape.squircle(...)(same iOS-style continuous-curvature look) and ships with its own exact, shader-matchedClipPathclipper (viaclipQuality: LiquidGlassClipQuality.exact). The old superellipse had no exact Flutter clip path, which forced a rectangle clip and capped its blur; the squircle's dedicated clipper now matches the SDF the shader draws and blurs correctly at any sigma. - Shapes are now selected via the
LiquidGlassShape.roundedRectangle(...)/.squircle(...)/.continuousRoundedRectangle(...)convenience constructors (a singlecornerStylevocabulary) instead of separateRoundedRectangleShape/SuperellipseShapeclasses. - Simplified the shaders: removed the superellipse SDF branch and the
u_shapeTypeuniform, so every lens now uses the analytic rounded-rect path. - Added
LiquidGlassView.regionCapture(off by default): per-lens region capture on the Skia sync path — each capture grabs only every lens's own rect (+ margin) instead of the whole background. A performance win when lenses cover a small part of a large background; no effect on Impeller.
2.0.1 #
- Fixed lens position being clamped to the parent bounds even when
outOfBoundaries: true. A lens moved past the parent's edge (in any direction) now keeps its true position instead of being pinned, so spacing between lenses stays correct.
2.0.0 #
- Added optical border mode (
OpticalBorder) — Apple-style, SDF-based rim lighting with background-tinted highlights, dual-sided specular reflections, and a lens height profile — alongside the existingClassicBorder. - Added new ready-made components:
LiquidGlassButton,LiquidGlassSearchBar,LiquidGlassAppIcon,LiquidGlassDock,LiquidGlassTabBar, andLiquidGlassBottomNavBar. - Improved rendering stability on release/profile builds with Impeller.
- Fixed lens and border content rendering upside down on Impeller's OpenGL ES backend (older Android devices) by inverting the texture sample Y-axis under
IMPELLER_TARGET_OPENGLES. - Breaking:
oneSideLightIntensityanddoubleSideLightIntensitymoved from the shape toClassicBorder. Pass them viaborderType: ClassicBorder(...)instead of directly on the shape.
1.1.1 #
- Formatted the dart files and changed the size of the thumbnail of screenshot.
1.1.0 #
- Added new refraction modes: shape refraction and radial refraction.
- Added new light modes: edge and radial.
- Added chromatic aberration support.
- Added one side light intensity support.
- Added saturation control.
- Updated magnification behavior to apply to the entire lens area rather than only the distortion region.
- Improved and optimized shader code.
- Removed
highDistortionOnCurves; the same effect can now be achieved by increasingdistortionand settingdistortionWidthto half of the smallest lens dimension.
1.0.0 #
Initial Stable Release – Liquid Glass Easy
- First official release of the
liquid_glass_easyFlutter package. - Provides real-time liquid glass lens effects with smooth distortion, magnification, and refraction.
- Built with shader-based rendering for high performance and flexibility.
- Includes
LiquidGlassViewandLiquidGlasswidgets for quick and easy integration into any UI. - Example app included to demonstrate usage, configuration, and visual styles.
- Ready for production and pub.dev distribution.