shift
shift moves the floating element along its alignment axis to keep it within the clipping boundary. Unlike flip (which changes the side), shift slides the element while keeping it on the same side.
import { computePosition, offset, flip, shift } from "@domphy/floating"
computePosition(reference, floating, {
placement: "top",
middleware: [offset(8), flip(), shift()],
})How It Works
When the floating element would overflow the start or end of the placement axis, shift nudges it just enough to bring the overflowing edge back inside the boundary.
Without shift: With shift:
┌────────────┐ ┌────────────┐
│ reference │ │ reference │
└────────────┘ └────────────┘
┌──────────┐ ┌──────────┐
│ floating │ │ floating │
└──────────┘ └──────────┘
clips right! back in viewPadding
Add space between the floating element and the boundary edge before the shift kicks in:
shift({ padding: 4 }) // 4px gap from all edges
// Per-side
shift({
padding: { top: 8, right: 4, bottom: 8, left: 4 },
})Cross Axis
By default shift only moves on the main axis (along the side). To also allow movement perpendicular to the placement side:
shift({ crossAxis: true })This is rarely needed and can cause the floating element to visually detach from the reference. Prefer flip for that axis.
middlewareData
shift writes to middlewareData.shift:
const { middlewareData } = await computePosition(reference, floating, {
middleware: [offset(8), shift()],
})
const { x, y, enabled } = middlewareData.shift!
// x: pixels shifted horizontally
// y: pixels shifted vertically
// enabled: { x: boolean, y: boolean } — which axes were shiftedlimitShift
Without a limiter, shift can slide the floating element far from the reference when the user scrolls. limitShift stops the shift when the reference and floating element's edges align, keeping the floating element visually anchored.
import { computePosition, offset, flip, shift, limitShift } from "@domphy/floating"
computePosition(reference, floating, {
placement: "top",
middleware: [
offset(8),
flip(),
shift({ limiter: limitShift() }),
],
})limitShift Options
limitShift({
// Start limiting earlier (+) or later (-) than the default edge-align point
offset: 4,
// Control which axes are limited
mainAxis: true, // default true
crossAxis: true, // default true
})offset can also be an object for asymmetric control:
limitShift({ offset: { mainAxis: 4, crossAxis: 8 } })Full Tooltip Example
A tooltip that stays visible during scroll but does not drift away from the reference:
import { toState } from "@domphy/core"
import { themeColor, themeSpacing } from "@domphy/theme"
import {
computePosition,
autoUpdate,
offset,
flip,
shift,
limitShift,
} from "@domphy/floating"
const open = toState(false)
let reference: HTMLElement | null = null
let floating: HTMLElement | null = null
let cleanup: (() => void) | null = null
function startPositioning() {
if (!reference || !floating) return
cleanup?.()
cleanup = autoUpdate(reference, floating, () => {
computePosition(reference!, floating!, {
placement: "top",
middleware: [
offset(8),
flip(),
shift({
padding: 8,
limiter: limitShift(),
}),
],
strategy: "fixed",
}).then(({ x, y }) => {
Object.assign(floating!.style, { left: `${x}px`, top: `${y}px` })
})
})
}
const App = {
div: [
{
button: "Hover me",
onMouseEnter: () => { open.set(true); startPositioning() },
onMouseLeave: () => { open.set(false); cleanup?.(); cleanup = null },
_onMount: (node) => { reference = node.domElement as HTMLElement },
},
{
div: "Tooltip text",
style: {
position: "fixed",
visibility: (l) => open.get(l) ? "visible" : "hidden",
backgroundColor: (l) => themeColor(l, "shift-12", "neutral"),
color: (l) => themeColor(l, "shift-1", "neutral"),
padding: themeSpacing(1),
borderRadius: themeSpacing(1),
fontSize: "0.875rem",
pointerEvents: "none",
},
_onMount: (node) => {
floating = node.domElement as HTMLElement
if (reference) startPositioning()
},
_onBeforeRemove: () => { cleanup?.(); cleanup = null },
},
],
}TypeScript
import type { ShiftOptions, LimitShiftOptions } from "@domphy/floating"
const shiftConfig: ShiftOptions = {
padding: 8,
mainAxis: true,
crossAxis: false,
limiter: limitShift({ offset: 4 }),
}
const limitConfig: LimitShiftOptions = {
offset: { mainAxis: 4, crossAxis: 0 },
mainAxis: true,
crossAxis: false,
}