mugen

Recipes

Chat, accordions, deep links, and shrink-to-fit bubbles.

AI chat with thinking & steps

A modern assistant transcript is a stress test for a virtualizer: every turn is a different height, reasoning traces collapse and expand, and the newest turn keeps growing as it streams. It maps cleanly onto mugen — each turn is a pure item → tree using all three hooks. useMugenState collapses the “Thought for…” trace (toggling re-measures just that row); useMugenEffect streams the final turn's answer in word by word; and stickToBottom follows it down with a smooth spring — scroll up to break free, scroll back to re-engage. It opens at the latest turn (initialScroll="bottom"); hit Replay to watch it stream again, or expand a reasoning trace and watch the list re-flow without losing your place:

ai-chat.tsx
loading preview…

Bidirectional pagination

Top and bottom slots are measured like row content, so skeleton loaders can live inside the scrollable coordinate system without throwing off row offsets. onTopReached and onBottomReached fire once when the scroll position enters their threshold, which is a clean fit for cursor pagination in either direction. This audit-log feed opens in the middle of a loaded page; hit either edge and the slot swaps in shimmering skeletons while the page resolves, then settles to the exact height of the prepended/appended variable-height rows — no jump. Idle edges show a soft hint, exhausted edges an end-cap:

pagination.tsx
loading preview…

Rich markdown rows

@wingleeio/mugen-markdown parses each row's markdown with incremark and renders it with mugen primitives — so headings, lists, fenced code, tables, and inline bold / code / links are all measured by the walker. Off-screen rows have exact heights with no measure-on-mount shift, even though every row is a different mix of blocks. Inline marks are styled through a deep-partial theme; block nodes are overridable with a fully-typed components map (here, h1 gets an accent rule) — both authored from the same primitives, so they stay measurable:

notes.tsx
loading preview…

Tooltips, popovers & dialogs in rows

Overlays are the case that breaks a virtualizer: a tooltip or dropdown is a real React component (hooks, raw DOM, a portal), so the walker can't measure it — and would throw if it tried. Escape makes that a non-issue: it reserves a fixed-size box in the row that the walker never looks inside, so a stock shadcn/ui or Radix widget drops in — trigger included — with no mugen-specific wrapper. The floating half is portaled to document.body by Radix itself, where mugen's layout never sees it. And short labels like a name or a role never wrap, so they don't need pretext either: plain styled DOM inside the declared box is exact by construction. This 800-row directory gives every row a shadcn Tooltip (hover the name), Popover (React), DropdownMenu (arrow-key menu), and a modal Dialog (Details) — open any of them and the row heights never budge:

directory.tsx
loading preview…

The contract is foreignObject's: you declare the box (height, optional width), and you design the children within it — content that outgrows the box is clipped, never silently re-measured. Keep Escape for known-footprint content; for a tooltip on measured, wrapping text, build a custom primitive whose measure() is measureChildren and whose render adds the trigger handlers — the walker measures the children exactly, and the floating panel portals out as usual.

Chat / message list

Avatar on the left, author + message on the right. The avatar is a fixed-size box (width/height); the text column grows into the remaining width — and every row is a different height.

chat-list.tsx
loading preview…

Accordion rows

useMugenState controls whether the body renders. Toggling re-measures the row. Click a question:

faq.tsx
loading preview…

Open a specific row's state and scroll to it — the scroll lands pixel-exact because the off-screen height is already known. Drive expansion from the item data so it works before the row ever mounts:

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

interface Thread {
  : string;
  : string;
  : string;
  : boolean; // expansion lives in the data
}

export function ({ ,  }: { : Thread[]; ?: string }) {
  const  = ({ :  });

  const  = (: string) => {
    // flip `open` for this thread in your own state, then:
    .(, { : 'smooth', : 'center' });
  };
  if () ();

  return (
    <
      ={}
      ={() => .}
      ="15px Inter"
      ={22}
      ={() => (
        < ={4} ={12}>
          < ="600 15px Inter">{.}</>
          {. ? <>{.}</> : null}
        </>
      )}
    />
  );
}

See Scrolling for the full scrollToItem API.

Shrink-to-fit bubbles

Chat bubbles that hug their text instead of filling the column:

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

interface Msg {
  : string;
  : string;
}

export function (: Msg) {
  return (
    < ={10}>
      < >{.}</>
    </>
  );
}

shrink measures the text at its natural (tightest) width via pretext's naturalWidth, and renders with width: fit-content so the browser shrink-wraps to the same width.

On this page