Pretext is a text measurement and layout library from Cheng Lou. TanStack Virtual still owns scrolling, range calculation, item positioning, and scroll-to behavior; Pretext can own the text-height estimate for rows whose height is mostly determined by wrapped text.
This is useful for chat logs, AI streams, activity feeds, comments, changelogs, notifications, and other text-heavy timelines where DOM measurement creates visible correction work.
Use Pretext when each virtual row's height can be derived from:
Do not make Pretext responsible for rows whose height depends on images, embeds, block markdown, loaded components, or arbitrary CSS layout. For those rows, use measureElement, call resizeItem when the extra content resolves, or split the text-only and non-text portions into separate sizing paths.
npm install @chenglou/pretextnpm install @chenglou/pretextCache prepare() by text and text-style inputs. Run layout() for the current width. When the width, font, line-height, or text options change, reset Virtual's measurements so offsets are recalculated from the new estimates.
import { clearCache, layout, prepare } from '@chenglou/pretext'
import { useVirtualizer } from '@tanstack/react-virtual'
const font = '14px Arial'
const lineHeight = 20
const preparedCache = new Map<string, ReturnType<typeof prepare>>()
function getPrepared(row: { id: string; text: string }) {
const key = `${row.id}:${font}:${row.text}`
const cached = preparedCache.get(key)
if (cached) {
return cached
}
const prepared = prepare(row.text, font, {
whiteSpace: 'pre-wrap',
letterSpacing: 0,
})
preparedCache.set(key, prepared)
return prepared
}
function estimateRowHeight(row: { id: string; text: string }, contentWidth: number) {
const text = layout(getPrepared(row), contentWidth, lineHeight)
const textHeight = Math.max(lineHeight, text.height)
return textHeight + 24
}
function Messages({ rows }: { rows: Array<{ id: string; text: string }> }) {
const parentRef = React.useRef<HTMLDivElement>(null)
const [width, setWidth] = React.useState(640)
React.useLayoutEffect(() => {
const element = parentRef.current
if (!element) {
return
}
const update = () => setWidth(element.clientWidth)
const observer = new ResizeObserver(update)
update()
observer.observe(element)
return () => observer.disconnect()
}, [])
const virtualizer = useVirtualizer({
count: rows.length,
getItemKey: (index) => rows[index]!.id,
getScrollElement: () => parentRef.current,
estimateSize: (index) => estimateRowHeight(rows[index]!, width - 32),
})
React.useLayoutEffect(() => {
virtualizer.measure()
}, [virtualizer, width])
React.useEffect(() => {
document.fonts.ready.then(() => {
preparedCache.clear()
clearCache()
virtualizer.measure()
})
}, [virtualizer])
return <div ref={parentRef}>{/* render virtual rows */}</div>
}import { clearCache, layout, prepare } from '@chenglou/pretext'
import { useVirtualizer } from '@tanstack/react-virtual'
const font = '14px Arial'
const lineHeight = 20
const preparedCache = new Map<string, ReturnType<typeof prepare>>()
function getPrepared(row: { id: string; text: string }) {
const key = `${row.id}:${font}:${row.text}`
const cached = preparedCache.get(key)
if (cached) {
return cached
}
const prepared = prepare(row.text, font, {
whiteSpace: 'pre-wrap',
letterSpacing: 0,
})
preparedCache.set(key, prepared)
return prepared
}
function estimateRowHeight(row: { id: string; text: string }, contentWidth: number) {
const text = layout(getPrepared(row), contentWidth, lineHeight)
const textHeight = Math.max(lineHeight, text.height)
return textHeight + 24
}
function Messages({ rows }: { rows: Array<{ id: string; text: string }> }) {
const parentRef = React.useRef<HTMLDivElement>(null)
const [width, setWidth] = React.useState(640)
React.useLayoutEffect(() => {
const element = parentRef.current
if (!element) {
return
}
const update = () => setWidth(element.clientWidth)
const observer = new ResizeObserver(update)
update()
observer.observe(element)
return () => observer.disconnect()
}, [])
const virtualizer = useVirtualizer({
count: rows.length,
getItemKey: (index) => rows[index]!.id,
getScrollElement: () => parentRef.current,
estimateSize: (index) => estimateRowHeight(rows[index]!, width - 32),
})
React.useLayoutEffect(() => {
virtualizer.measure()
}, [virtualizer, width])
React.useEffect(() => {
document.fonts.ready.then(() => {
preparedCache.clear()
clearCache()
virtualizer.measure()
})
}, [virtualizer])
return <div ref={parentRef}>{/* render virtual rows */}</div>
}See the React Pretext example for a complete chat-style implementation: React Pretext.