@pretext-studio/core
v0.2.0
Published
React hooks for pixel-perfect text layout prediction — no DOM reflow, no layout shifts
Maintainers
Readme
@pretext-studio/core
React hooks for pixel-perfect text layout prediction — no DOM reflow, no layout shifts.
Built on top of @chenglou/pretext, a pure-JS text layout engine that computes line breaks and heights from font metrics alone — before anything renders.
npm install @pretext-studio/core @chenglou/pretextWhy
The browser can't tell you how tall a block of text will be until it lays it out. This forces one of two bad patterns:
- Guess and adjust — render first, measure with
getBoundingClientRect, update. Causes layout shift. - Arbitrary caps —
max-height: 9999pxfor accordion animation. Causes broken easing.
@pretext-studio/core gives you exact pixel heights and line counts before paint, with zero DOM reads.
Installation
npm install @pretext-studio/core @chenglou/pretextBoth packages are required. @chenglou/pretext is a peer dependency — the layout engine that does the measurement.
Hooks
useTextLayout
Predicts the height and line count of a block of text at a given width.
import { useTextLayout } from '@pretext-studio/core'
function Article({ text }: { text: string }) {
const { height, lineCount, isReady } = useTextLayout({
text,
font: '16px Inter, sans-serif',
width: 640,
lineHeight: 24,
})
return (
<div style={{ minHeight: isReady ? height : undefined }}>
{text}
</div>
)
}Options
| Prop | Type | Description |
|------|------|-------------|
| text | string | The text to measure |
| font | string | CSS font string — must match what the browser uses |
| width | number | Container width in px |
| lineHeight | number | Line height in px |
Returns { height, lineCount, isReady }
useBubbleMetrics
Finds the tightest possible bubble width that keeps the same line count. Eliminates the dead space CSS fit-content leaves on short last lines.
import { useBubbleMetrics } from '@pretext-studio/core'
function ChatBubble({ text, side }: { text: string; side: 'sent' | 'recv' }) {
const { tightWidth, height, isReady } = useBubbleMetrics({
text,
font: '15px Helvetica Neue, sans-serif',
lineHeight: 20,
maxWidth: 280,
paddingH: 12,
paddingV: 8,
})
return (
<div style={{
width: isReady ? tightWidth : undefined,
height: isReady ? height : undefined,
alignSelf: side === 'sent' ? 'flex-end' : 'flex-start',
}}>
{text}
</div>
)
}Options
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| text | string | — | Message text |
| font | string | — | CSS font string |
| lineHeight | number | — | Line height in px |
| maxWidth | number | — | Max bubble width including padding |
| paddingH | number | 12 | Horizontal padding each side |
| paddingV | number | 8 | Vertical padding each side |
Returns { tightWidth, height, lineCount, wastedPixels, isReady }
useStableList
Pre-computes heights for a list of items so the list can be rendered without layout thrash. Useful for virtualized lists, masonry grids, and any list where you need heights before render.
import { useStableList } from '@pretext-studio/core'
function MessageList({ messages }: { messages: { id: string; text: string }[] }) {
const { heights, totalHeight, isReady } = useStableList({
items: messages,
font: '15px Helvetica Neue, sans-serif',
width: 360,
lineHeight: 22,
paddingV: 16,
})
return (
<div style={{ height: totalHeight, position: 'relative' }}>
{isReady && messages.map((msg, i) => {
const top = messages.slice(0, i).reduce((sum, m) => sum + (heights.get(m.id) ?? 0), 0)
return (
<div key={msg.id} style={{ position: 'absolute', top, height: heights.get(msg.id) }}>
{msg.text}
</div>
)
})}
</div>
)
}Options
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| items | { id: string; text: string }[] | — | List items |
| font | string | — | CSS font string |
| width | number | — | Container width in px |
| lineHeight | number | — | Line height in px |
| paddingV | number | 16 | Vertical padding per item |
Returns { heights: Map<string, number>, totalHeight, isReady }
MeasuredText
A drop-in component that pre-sizes itself to the predicted height and optionally reports prediction mismatches for debugging.
import { MeasuredText } from '@pretext-studio/core'
<MeasuredText
text="The quick brown fox..."
font="16px Inter, sans-serif"
width={640}
lineHeight={24}
debug={process.env.NODE_ENV === 'development'}
onMismatch={({ predicted, actual, delta }) => {
console.warn(`Layout mismatch: predicted ${predicted}px, got ${actual}px (Δ${delta}px)`)
}}
/>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| text | string | — | Text content |
| font | string | — | CSS font string |
| width | number | — | Container width in px |
| lineHeight | number | — | Line height in px |
| debug | boolean | false | Show predicted height/line overlay |
| onMismatch | (m: LayoutMismatch) => void | — | Called when actual height differs by >2px |
| style | React.CSSProperties | — | Extra styles on the wrapper div |
| className | string | — | Class name on the wrapper div |
clearAllCaches
The SDK caches prepare() results internally (LRU, max 2000 entries). Call this if you need to free memory explicitly.
import { clearAllCaches } from '@pretext-studio/core'
clearAllCaches()The font string
The font prop must exactly match the CSS font the browser will use — same family, size, weight, and style. A mismatch between the font you pass and the font the browser renders will produce wrong measurements.
// Match your CSS exactly
font: '16px Inter, sans-serif'
font: '500 15px "Helvetica Neue", Helvetica, Arial, sans-serif'
font: 'italic 14px Georgia, serif'If you're using a web font, make sure it's loaded before measurements run. Use the isReady flag to gate renders until the font is available.
TypeScript
All hooks and the component are fully typed. Key types:
interface TextLayoutOptions {
text: string
font: string
width: number
lineHeight: number
}
interface TextLayoutResult {
height: number
lineCount: number
isReady: boolean
}
interface BubbleMetrics {
tightWidth: number
height: number
lineCount: number
wastedPixels: number
}
interface LayoutMismatch {
predicted: number
actual: number
delta: number
}License
MIT
