mugen

Scrolling

Drive scroll imperatively through the list instance.

The instance from useMugenVirtualizer is your handle on the list. Because every row's offset is known analytically — on- or off-screen — scrolling to a specific item lands pixel-exact on the first try, with no measure-and-correct dance.

import { , , ,  } from '@wingleeio/mugen';

interface Row {
  : string;
  : string;
}

export function ({  }: { : Row[] }) {
  const  = ({ :  });

  return (
    <>
      < ={() => .('42', { : 'smooth', : 'center' })}>
        Reveal #42
      </>
      <
        ={}
        ={() => .}
        ="15px Inter"
        ={22}
        ={() => (
          < ={12}>
            <>{.}</>
          </>
        )}
      />
    </>
  );
}

Methods

Method
scrollToItem(key, options?)Scroll a row into view by its getKey value.
scrollToIndex(index, options?)Scroll a row into view by index.
lengthNumber of items.
totalHeight()The exact scrollable height (px) — every row counted.

Options

interface ScrollToOptions {
  ?: 'auto' | 'smooth';
  ?: 'auto' | 'start' | 'center' | 'end';
}
  • behavior'smooth' animates; 'auto' (default) jumps.
  • align — where the row lands: start (top), center, end (bottom), or auto (default) which scrolls the least — a no-op if the row is already fully visible, otherwise to the nearest edge.

Call these from anywhere you hold the instance — an effect, an event handler, a deep-link router. The instance is stable across renders.

Reach callbacks

Use edge callbacks for pagination. They fire once when the scroll position enters the configured pixel threshold, then arm again after the user leaves that edge:

<MugenVList
  instance={list}
  getKey={(row) => row.id}
  render={Row}
  renderTop={() => <TopLoader loading={loadingPrevious} />}
  renderBottom={() => <BottomLoader loading={loadingNext} />}
  onTopReached={loadPreviousPage}
  onBottomReached={loadNextPage}
  topReachedThreshold={24}
  bottomReachedThreshold={24}
/>

renderTop and renderBottom are scrollable slots, not sticky overlays. Their heights are measured with the same primitive walker as rows, so scrollToItem, totalHeight(), and distanceFromBottom stay in the same coordinate system.

Start at the bottom

Chat-style lists open at the newest message, not the oldest. Pass initialScroll="bottom" to <MugenVList> and it opens scrolled to the end:

<MugenVList instance={list} getKey={(m) => m.id} render={Message} initialScroll="bottom" />

It's applied once, after the first measure and before paint — no top-to-bottom flash, no blank gap. Because every row's height is already known, the landing is exact even though those rows have never mounted. The default is "top". To animate the landing instead of jumping, pass an object:

<MugenVList initialScroll={{ to: 'bottom', behavior: 'smooth' }} />

You can also open on a measured row by index:

<MugenVList initialScroll={{ to: 'index', index: 40, align: 'center' }} />

This is applied before paint, like top and bottom. If later data changes insert rows above the visible keyed content, mugen preserves the current visual anchor by shifting scrollTop by the inserted height. That keeps top pagination from dumping the user onto the newly prepended page.

Stick to the bottom while streaming

For a chat that streams tokens, set stickToBottom and the list stays pinned to the bottom as content grows — following each new token with a velocity-based spring. The instant the reader scrolls up it lets go; when they return to the bottom it re-engages:

<MugenVList initialScroll="bottom" stickToBottom />

Tune the spring — or snap instantly — by passing options:

<MugenVList

  stickToBottom={{ behavior: 'smooth', damping: 0.7, stiffness: 0.05, mass: 1.25, threshold: 70 }}
/>

threshold is how close to the bottom (px) still counts as "stuck". See both in the AI chat recipe: it streams the last turn and follows it down, and you can scroll up to break free or hit Replay.

On this page