lift 1.4.1
Fixed
- An open lift keeps the page still on iOS: a touch on a menu that doesn’t scroll, or one at its end, no longer scrolls the page under it.
Every release, newest first. Since 2.0 the four packages come out together, at one version; before, each feature was a package of its own.
@fluidkit/core@fluidkit/react@fluidkit/vue@fluidkit/svelte
Breaking
Fluid Kit 2.0: one package for each framework instead of one for each feature. An app installs one: @fluidkit/react, @fluidkit/vue, @fluidkit/svelte, or @fluidkit/core without a framework. All four come out together, at the same version, from now on.
@fluidkit/core has every engine (Liquid Blur’s press and groups, morph, indicator, drag, lift and the new layout), the spring and Motion below, and one stylesheet, @fluidkit/core/style.css: the glass and the springs and transitions for CSS. Nothing the app doesn’t import ends up in its bundle; features still meet only through the DOM.
@fluidkit/react, @fluidkit/vue and @fluidkit/svelte have every feature’s components and hooks (composables, runes) and the helpers they’re made of, and give all of core besides: springs or createLayout come from the same package, and so does style.css. Core’s engine types named as the components are (Drag, Indicator, Layout, Lift, Morph) are there as DragEngine and so on.
What the features do and take hasn’t changed; only where they’re imported from:
| Before | Now |
|---|---|
@fluidkit/morph, @fluidkit/indicator, @fluidkit/drag, @fluidkit/lift |
@fluidkit/core |
@fluidkit/blur/press, @fluidkit/blur/melt |
@fluidkit/core |
@fluidkit/blur/auto |
@fluidkit/core/auto |
@fluidkit/blur/style.css |
@fluidkit/core/style.css, or your framework’s package’s style.css |
@fluidkit/<feature>/react, @fluidkit/core/react |
@fluidkit/react |
@fluidkit/<feature>/vue, @fluidkit/core/vue |
@fluidkit/vue |
@fluidkit/<feature>/svelte, @fluidkit/core/svelte |
@fluidkit/svelte |
@fluidkit/blur, @fluidkit/morph, @fluidkit/indicator, @fluidkit/drag and @fluidkit/lift stay on npm as they are, and get no more releases.
New in 2.0:
createLayout({ root }) watches a root and its items (its children, or items as a selector) and follows rows put in, taken out or reordered, their class and hidden, and their sizes, before the browser paints the change. Every item that moved is shown where it was drawn and goes to where it is on a spring (translate); another change on the way sets off from where everything is drawn then, keeping its speed. Sizes change for real: an item whose size changed, and the root itself, change width and height on the spring, clipped along that axis meanwhile, so text isn’t squeezed and what comes after them moves with them; size: false moves positions only. A new item comes in from transparent and a little smaller (enter, or any of the transitions: blurred, moved); one taken out goes out where it was (exit), the element itself put back for the while, inert and hidden from assistive technology, then removed. Put back meanwhile, it stays. Its own translation (a drag’s offset) is added to, its own scale multiplied, and transform is left alone, so a glass press and a morph go on; an item being dragged isn’t moved. spring, onStart, onRest; moving, update(), set(), destroy(). Reduced motion is all at once, and so is a change while the root isn’t drawn (hidden in a tab not shown, say) and its showing again. Layout.Root and useLayout() for each framework.springs.smooth(), springs.snappy(), springs.bouncy() and springs.interactive() give a SpringParams for any spring; given { duration, extraBounce }, they tune it, as SwiftUI’s .snappy(duration:extraBounce:) does. transitions.opacity(), transitions.scale(), transitions.blurReplace() and transitions.move() give a TransitionParams ({ opacity, scale, blur, x, y }): how an element comes in and goes out, for Layout’s enter and exit, combined by spreading. springCss(params) writes a spring for CSS: its own curve as a linear() easing, and how long it takes to come to rest. In the stylesheet, each spring is --fk-<name> (and its -easing and -duration), and fk-transition with fk-opacity, fk-scale, fk-blur-replace, fk-move and the springs’ classes brings an element in as it’s put in the page or shown and takes it out as it’s hidden (hidden, a <dialog> or popover closed), tuned with variables. Reduced motion only fades.Each feature was a package of its own, released when it changed. Those packages are deprecated now: their features are in the four above, as they were.
Fixed
Fixed
lb-edge, lb-edge-x) show in minified builds. Given animation and animation-timeline, Lightning CSS (Vite’s minifier) folded the timeline into the shorthand, which browsers reject, so the fade never showed; the animation is now written in longhands.Fixed
Glass from @fluidkit/blur/vue type-checks in an app that checks libraries (skipLibCheck: false): its declaration no longer spells out Vue’s instance type, which vue-tsc rejected.Fixed
defaultValue no longer decides the type of the values in React and Vue, as it already didn’t in Svelte: useMorph({ defaultValue: "a" }) now takes to("b"), and useIndicator({ defaultValue: "a" }) takes select("b"), without naming the type.New
Svelte components and functions: @fluidkit/lift/svelte, @fluidkit/indicator/svelte, @fluidkit/morph/svelte, @fluidkit/drag/svelte and @fluidkit/blur/svelte. Svelte is an optional peer dependency (5.29+, for attachments). The components ship as .svelte files your bundler compiles.
child snippet, as in bits-ui: spread the props it gives, which carry the attachment that connects it. State is bindable (bind:open, bind:value, bind:snap), and a getter and setter binding lets you refuse a change. Events are callback props (onSelect, onValueChange, onSnapChange, …).Lift.Root, Lift.Target, Lift.Menu, Lift.Item, Lift.Separator, Lift.Preview, plus useLift().Indicator.Root, Indicator.Thumb, Indicator.Item, plus useIndicator().Morph.Root, Morph.State, Morph.Trigger, Morph.Match, plus useMorph(). The animation starts after the new value renders; states added or removed later are handled automatically.Drag.Root, Drag.Handle, Drag.Scroll, plus useDrag(). Snap points and bounds can be a function of the parent’s size.Glass and Glass.Group, plus glass() (returns { class, style } to spread), usePress() and useGlassGroup().@fluidkit/core/svelte, the helpers they’re built from: mergeProps, PartProps, elementRef, syncSettings.use*() functions take their options as a getter and return attachments for {@attach}. Call them in a component’s script.New
Vue components and composables: @fluidkit/lift/vue and @fluidkit/indicator/vue. Vue is an optional peer dependency (3.5+).
Lift.Root, Lift.Target, Lift.Menu, Lift.Item, Lift.Separator, Lift.Preview; Indicator.Root, Indicator.Thumb, Indicator.Item. Namespaced, so <Lift.Root> works in <script setup> templates.as-child, as in Reka UI; the slot receives the part’s state. State is exposed as the same data attributes.v-model:open on Lift.Root, v-model on Indicator.Root, plus active and @active-change. A lift item’s @select event can call preventDefault() to keep the menu open.useLift() and useIndicator() return function refs for :ref. Options can be an object, a ref or a getter.set() when props change. Server rendering (including Nuxt) and unmounting while a lift is open work.@fluidkit/core/vue: the helpers the adapters are built from (renderPart, attachRef, useControllable, watchSettings…).New
core, indicator, lift: React: @fluidkit/lift/react and @fluidkit/indicator/react, React an optional peer (18 or later).
Lift.Root, Lift.Target, Lift.Menu, Lift.Item, Lift.Separator, Lift.Preview; Indicator.Root, Indicator.Thumb, Indicator.Item. Each part renders as an element of yours with render, as in Base UI (render={<img src={src} />}, or a function of its props and state), our props merged into it. Headless: their state is in data attributes (data-state, data-selected, data-active, data-disabled) for any CSS.open / defaultOpen / onOpenChange for a lift; value / defaultValue / onValueChange for an indicator, and active for an item pointed at from elsewhere (the finger on a lift’s menu, say). A lift’s items have values, onSelect on the root is given the one chosen, and an item’s own onSelect can preventDefault() to keep the menu open.useLift() and useIndicator() give refs to bind the engine to your elements, its state and the engine itself.set(), never made again for them; callbacks are always the latest. Strict mode, React 18’s and 19’s refs, server rendering and unmounting while a lift is open all work.@fluidkit/core/react: what the adapters are made of (renderPart, mergeProps, useMergedRefs, useAttach, useControllable, useSettings…), for a component of your own.blur, morph, drag: React components and hooks: @fluidkit/morph/react, @fluidkit/drag/react and @fluidkit/blur/react. React is an optional peer dependency (18+).
Morph.Root, Morph.State, Morph.Trigger and Morph.Match, plus useMorph(). States have a value (value / defaultValue / onValueChange), and callbacks receive values instead of elements. A state can also be a trigger via render. The animation starts after the new value renders, so labels and matches changed by the same click animate correctly. States added or removed later are handled automatically.Drag.Root, Drag.Handle and Drag.Scroll, plus useDrag(). Snap points and bounds can be a function of the parent’s size, recomputed on resize and re-render. snap / defaultSnap / onSnapChange; if the parent keeps snap unchanged, the element goes back to it.Glass with variants as props (clear, tint, lens, transparency, depth, interactive, highlight, swell, stretch), which sets up press effects automatically, and Glass.Group for glass groups. Also glass(), usePress() and useGlassGroup().morph, indicator, drag, lift: set(settings): a lift, drag, indicator or morph takes new options once made, the rest kept, without being made again: what a component needs as its props change. Everything but the elements it’s made on.
element and menu. Open, it takes them as it goes (its dim, blur, callbacks) or the next time it opens. LiftSettings is their type.element, handle, axis, scroll and initial. New snap points or bounds are followed at once, as by update(). DragSettings.container, indicator and rest, which is a property of its own. A new trigger (or focus) takes over at once, and what pointed at it under the old one lets go. IndicatorSettings.spring and callbacks; new springs take the next flight. MorphSettings.lift: onSelect giving back false keeps the menu open: an item chosen that doesn’t close it, a toggle in the menu, say. Anything else closes it, as before.
New
A strict review’s fixes, and keys for the menu.
preview keeps its own size, centered on the element and kept on screen, as iOS’s bigger previews are; one taken from the page goes back where it was, its style and attributes as they were. It was squeezed into the element’s box and left out of the page.:disabled, aria-disabled="true") is never chosen, a click on it included; opened by a key, the first item focused is the first enabled one.New
SpringAnimator.timeScale: how fast every spring runs, all of them at once. 0.25 is four times slower: slow motion to look at a transition, or to show one in a bug report. Every package’s springs run on core’s, so one setting slows them all.Fixed
offsetWidth is rounded, and was taken for a scale.Breaking
First release.
createLift({ element, menu }), as iOS’s context menus.<dialog> over any overflow, z-index and transform; the element is hidden where it is and keeps its room. preview lifts an element of your own instead: something live, or a bigger preview.data-highlighted and handed to onHighlight; let go without moving, the menu stays open. A choice clicks the item, calls onSelect and closes the menu.delay (500ms), it lifts; moved, or taken by a scroll, it doesn’t. data-lifting while it’s held, data-lifted while open; no text selected, no iOS callout, and the click a long press would end in is dropped.scale), the dim and the menu coming in with it, and settles exactly back before the element shows again. dim, blur, placement, spring; onOpen, onClose; opened, open(), close(), destroy(). Escape closes it, focus goes back; the page doesn’t scroll under it; reduced motion is all at once.@fluidkit/core.New
--lb-edge-start and --lb-edge-end size one side’s fade (the top or left, the bottom or right), 0px turning that side’s edge off; --lb-edge-strength sets how transparent the content gets at the edge, all the way (1) by default.Fixed
holdSelection writes -webkit-user-select before user-select, holding and letting go, so where the prefixed one is an alias of the other the root’s own user-select is what’s left.New
holdSelection(document): no text is selected on the page until it’s let go, for a gesture that pulls something around: the root’s user-select is none and iOS’s touch callout off, a selection already there is dropped, and holds count, so overlapping gestures share one.lb-edge on an element that scrolls fades its content out toward its top and bottom, as under the bars in iOS; lb-edge-x, toward its sides. An edge shows only while there’s content past it, so at either end of the scroll its edge is gone and covers nothing. Pure CSS, a mask driven by scroll-driven animations; --lb-edge-size (28px) sets how far it reaches. Without scroll-driven animations there are no edges.Fixed
Fixed
New
lb-lens, or --lb-lens from 0 to 1. The same glass with no blur, so what’s under it shows sharp, as a thumb that’s held is in iOS. --lb-lens is inherited and registered, so it transitions and any selector can set it: .thumb[data-dragging] { --lb-lens: 1 } turns a held thumb into a lens and back. --lb-transparency is registered now too, so it transitions as well.--lb-stretch-axis: which way lb-stretch pulls, x, y or both (the default), read once per press like --lb-stretch. A switch’s thumb that stays in its track stretches along x only.follow: a spring the element chases the pointer on while dragged, instead of sitting exactly under it, so a pointer flung back and forth is followed through, as iOS 27’s switches do. Where a flick goes is still judged from the pointer. And a first move that goes far past the slop at once (a pointer flung) now moves the element by all but the slop, where it used to be lost.New
scroll: an area inside that scrolls along the drag’s axis, such as a sheet’s list, shares one finger’s gesture with the element, as iOS sheets do. A finger in the list moves the sheet when it pulls toward the later snap points with the list at its start, or pushes back while the sheet isn’t at its first; else the list scrolls with its own momentum. A sheet pushed all the way hands the rest of the push to the list, and pulled back, the list scrolls back to its start before the sheet moves; a list scrolled back to its start and still pulled hands the gesture to the sheet where the browser lets the scroll be stopped. With scroll, the handle’s touch-action is left alone: fingers are followed by their touch events.Breaking
drag: First release.
createDrag({ element }), along x, y or both, following the pointer exactly, in the element’s own pixels (a scaled container works). It moves by its inline translate alone, so transform and scale stay free for the page and press behaviors.bounds (the farthest snap points unless given), on iOS’s curve, dragged or swinging past them on its spring; rubberband: 0 stops it there instead.deceleration), so a flick reaches a far point and a slow drag settles on the near one. With no snap points it comes to rest where it would have, within its bounds. Snap points are offsets along the axis, or points for both; snap points and bounds given as functions are read again when the element, its parent or the window is resized.touch-action leaves the other axis (and pinch-zoom) to the browser. The click a drag would end in is dropped; text fields don’t start one; text isn’t selected and links aren’t dragged off while it goes.handle, initial, spring; onStart, onMove with the position and speed, onEnd with the snap point, onRest; position, velocity, dragging, moving, current, disabled, snap(), to(), update(), destroy(). data-dragging while a pointer drags it. With reduced motion it’s on its point at once when let go.@fluidkit/core.New
swallowClick(document, except?, ms?) drops the click a gesture’s release would make on its own, as drag and indicator need: the next click in the document within 400ms, but for one on except.Fixed
popover attribute, which hid them (a popover that isn’t open is display: none).First release.
SpringAnimator: SwiftUI-style springs (perceptual duration and bounce) over several numeric channels, shared or per channel, solved exactly at any frame rate. to() keeps the velocity when retargeting; retarget() corrects the flight under way without a second overshoot; jump() moves channels at once, keeping their velocity; velocities() reads each channel’s speed to hand it on; an optional settle bounce after the first turn. A frame stamped before the movement was asked for doesn’t step it back.measureBox() (where an element is on screen, its corners and their shape), radiiOf(), cornerOf() and fitRadii() for circular corners fitted the way the browser fits them, cornerShapeOf() for corner-shape as a superellipse exponent, drawsCornerShape(), radiiCss(), px(), and still() to read an element without its inline transforms.SURFACE, PART and AWAY (data-fk-surface, data-fk-part, data-fk-away).First release.
@fluidkit/blur/press): spring-driven highlight, swell and stretch, for mouse, touch and keyboard; disabled controls (:disabled, aria-disabled="true") aren’t pressed. @fluidkit/blur/auto installs it on import.@fluidkit/blur/melt): glass groups that merge nearby rounded shapes into one surface without blurring overlaps twice. The shared glass is clipped and masked to the melted outline, so Chromium on Windows doesn’t show a blurred rectangle around it under a rounded, clipping ancestor.@fluidkit/morph through the Fluid Kit DOM contract (data-fk-surface, data-fk-part, data-fk-away), without importing it.@fluidkit/core.Compatibility
backdrop-filter, color-mix() and CSS pow(); registered properties have fallbacks. The stylesheet keeps -webkit-backdrop-filter for Safari before 18.clip-path: path() and Canvas 2D.First release.
createMorph({ states }) and to(state), between any of its states, with independent springs for center, size and progress, interruptible with the velocity kept. Sent back mid-way it turns around; sent elsewhere, it starts over from where the shape is.translate: -50% -50%) is kept.data-fk-match in two states fly from one to the other.@fluidkit/blur it’s the same glass, and inside a glass group it lifts over the group or melts with it, through the Fluid Kit DOM contract.corner-shape is supported.update() for any other move.overflow).opacity: 0, data-fk-away); pressing the shape on its way presses the state under it.current, moving, a promise from to(); { instant: true }; initial; onStart, onRest and a per-frame onProgress; add() and remove() for states that come and go; partial grow and shrink springs; a container for the shape out of a clipping parent.@fluidkit/core.Compatibility
First release.
createIndicator({ container, indicator }), its center, size and corners on interruptible springs that keep their velocity, without overshoot by default, a few frames from item to item.trigger: hover for a mouse or a pen and press for a finger, per event (auto), or hover, press, manual. A press shows after a short delay on touch, so a finger that lands to scroll shows nothing; released over another item, that item is clicked (click). Keyboard focus that shows moves it too (focus).data-pressed on the indicator while a pointer is down on its item (a press, or a click on the hovered one), for the page to style.rest, where it stays when nothing points, such as the chosen tab; to(), current, update(), destroy(); partial springs; onChange.corner-shape is supported. An item with square corners lends it the indicator’s own.@fluidkit/core.Compatibility
z-index: -1, or positioned items). Rotated ancestors aren’t supported.touch-action: none and user-select: none. A press the browser takes over (a drag of selected text) ends without a choice.