Affix

Pins an element to the viewport once the page scrolls past it, and puts a placeholder of the same size in its place so nothing below jumps.

Usage

Affix pins its child once its scroll container passes it, and leaves a placeholder of the same size behind so nothing below jumps. Most of the time position: sticky does that job with no JavaScript. The sections below cover when it cannot, and what position: fixed does to the layout around it.

When to use, and what to use instead

Try position: sticky first. It needs no JavaScript and covers a table header or a section heading. Affix does the same job by measuring its child on scroll and switching it to position: fixed.
Use Affix in two cases:
  • Pinning to the bottom edge with offsetBottom, which sticky cannot do.
  • Pinning something with an overflow or a transform on an ancestor. Either one stops sticky from working, with no error.
Use something else for a table header (Table has its own sticky header), for a container that scrolls sideways (use position: sticky), or for an element in a column that already pins itself.

offsetTop and offsetBottom

offsetTop is the distance from the top of the scroll container at which the child pins. It defaults to 0 when neither offset is set, so offsetBottom on its own pins to the bottom only.
offsetBottom is the distance from the bottom edge: a save bar under a long form, a cart total, a wizard's Next button.
Set one offset. With both, the child pins to the bottom edge until you reach it and to the top edge once you scroll past it.

target: which element scrolls

Inside a scrolling panel, pass target. It defaults to the window (or ConfigProvider's getTargetContainer), so an Affix inside a drawer, a modal or an overflow: auto panel never pins. Pass a function that returns the panel: target={() => panelRef.current}.
  • Re-measure when the page scrolls. An Affix with a target listens only to that element. If the panel itself moves with the page, call updatePosition() on the ref from a window scroll and resize listener.
  • A function that returns null attaches no listeners, and nothing tells you. A ref read before mount does that.

The placeholder, and updatePosition()

When the child pins, Affix leaves a hidden placeholder of the same size in the page, so the content below stays where it was.
Call updatePosition() when the child changes size. The placeholder's size is recorded when the child pins. Without the call, it keeps the old size and the content below is off by the difference.
  • The child must not be position: absolute. Set absolute positioning on Affix's own style instead.
  • style and className go on the outer element, the one that stays in the page, not on the pinned one.
  • Give Affix one element with its own size. A bare text node or several siblings measure unpredictably.

Reacting with onChange

onChange fires with true when the child pins and false when it returns, once per change rather than on every scroll. Use it to add a shadow to a pinned toolbar or swap a heading for a compact one.
  • Keep the handler cheap. It runs while the page scrolls.
  • If the handler changes the child's height, call updatePosition() after it. A toolbar that shrinks when it pins is the common case.
  • Do not use it to mount or unmount the child. The placeholder keeps the size of an element that is no longer there.

What position: fixed does to its ancestors

A pinned child is position: fixed, and that has two effects on the layout around it.
  • It escapes clipping. An ancestor's overflow: hidden no longer contains it, so it can paint over the next thing on the page, including the footer.
  • Keep transform, filter and contain off its ancestors. Any of them makes the pinned child position itself against that ancestor instead of the viewport, so it lands in the wrong place. An entry animation that keeps its final transform does the same, so use animation-fill-mode: backwards.

Never nest an Affix inside a sticky rail

An Affix inside a position: sticky column leaves the column when it pins, so it keeps moving with the viewport and paints over the footer.
Anchor does this by default. It wraps its links in an Affix unless you pass affix={false}, so pass it when the Anchor already sits in a sticky rail.
Pick one of sticky and Affix per column. If the outer element already pins itself, the inner one should only scroll.