
If you have ever built a chat thread, card feed, whiteboard label editor, or masonry layout in Vue, you have probably ended up doing something slightly gross: render text into the DOM, measure it, and then rerender or reposition everything based on that measurement.
That works, but it comes with baggage:
getBoundingClientRect() and offsetHeight reads in hot pathsPretext is interesting because it attacks that exact problem. Instead of asking the DOM how tall wrapped text became, it prepares the text once and then lays it out against a width using cached measurements. In other words, you can know a text's height before it is even mounted so that Vue can stay focused on state and rendering while Pretext handles the text math.
This is not a replacement for Vue, CSS, or the browser layout engine. It is a way to stop using the DOM as your text calculator when all you really need is line count and height.
The core model is simple:
prepare(text, font) does the expensive work once.layout(prepared, width, lineHeight) returns the wrapped height and line count for that width.That split matters. If the text and font stay the same while the available width changes, you do not need to re-measure every grapheme from scratch on every resize. You reuse the prepared value and run layout again.
That makes Pretext especially compelling in UI that has lots of text blocks with widths changing over time:
Vue is already very good at state transitions. The problem is that text measurement traditionally drags you back into imperative DOM work.
You start with clean reactive code:
const messages = ref<Message[]>([]);Then layout requirements show up and suddenly you are doing things like:
await nextTick();
const height = node.getBoundingClientRect().height;That is the line I would try to delete first.
With Pretext, the flow looks more like this:
That is a much cleaner separation of concerns.
ni @chenglou/pretextThe main trick is to make prepare() depend on the text and font, but not the width. Width changes should only trigger layout().
import { computed, toValue } from "vue";
import { layout, prepare } from "@chenglou/pretext";
export function usePretextLayout(options) {
const text = computed(() => toValue(options.text));
const font = computed(() => toValue(options.font));
const width = computed(() => toValue(options.width));
const lineHeight = computed(() => toValue(options.lineHeight));
const prepared = computed(() => prepare(text.value, font.value));
const result = computed(() =>
layout(prepared.value, Math.max(1, width.value), lineHeight.value),
);
const height = computed(() => result.value.height);
const lineCount = computed(() => result.value.lineCount);
return {
text,
font,
width,
lineHeight,
prepared,
result,
height,
lineCount,
};
}Why split it this way?
That maps nicely onto Vue's computed graph.
Here is a minimal card example. The width is reactive, but the text measurement does not require mounting a hidden probe element just to discover its height.
<script setup lang="ts">
import { ref } from "vue";
import { usePretextLayout } from "./composables/usePretextLayout";
const body = ref(
"Pretext lets Vue apps estimate wrapped text height without measuring hidden DOM nodes first.",
);
const cardWidth = ref(320);
const font = ref("400 16px Inter, system-ui, sans-serif");
const lineHeight = ref(24);
const { height, lineCount } = usePretextLayout({
text: body,
font,
width: cardWidth,
lineHeight,
});
</script>
<template>
<article class="card" :style="{ width: `${cardWidth}px` }">
<p>{{ body }}</p>
<footer>{{ lineCount }} lines, {{ height }}px tall</footer>
</article>
</template>This is the important mindset shift: the card height is no longer something you discover after the browser lays out the paragraph. It is something you can derive from the same reactive inputs that already describe the UI.
The simplest demo is a single card, but that is not where the real value is. The real value shows up when the old approach creates layout thrash or architectural awkwardness.
Virtualization loves predictable heights. Text-heavy UIs often do not have them.
Pretext gives you a better story:
That is much better than mounting off-screen rows just to measure them.
import { layout, prepare, type PreparedText } from "@chenglou/pretext";
type Row = {
id: string;
body: string;
prepared: PreparedText;
};
const font = "400 15px Inter, system-ui, sans-serif";
const lineHeight = 22;
const rows: Row[] = apiRows.map((row) => ({
id: row.id,
body: row.body,
prepared: prepare(row.body, font),
}));
function getRowHeight(row: Row, contentWidth: number) {
return layout(row.prepared, contentWidth, lineHeight).height + 24;
}That pattern fits Vue very naturally. You can prepare once when rows are normalized, then derive heights wherever your layout logic needs them.
If your message UI wants to make decisions based on line count or wrapped height, Pretext is a cleaner primitive than "render first, inspect later."
Examples:
Those are layout decisions based on text shape, not business logic. They should not require DOM probes in every component instance.
This is where Pretext starts to feel like a category change rather than a small optimization.
Its advanced APIs, including prepareWithSegments(), layoutWithLines(), and layoutNextLine(), are designed for cases where you need more than total height:
That is useful for labels on canvases, text around shapes, or any UI where the browser is not directly painting the final text layout for you.
Pretext is not guessing in the abstract. It measures text against a font declaration and then lays out against a width and line height.
That means two values need to match what your UI actually renders:
font string you pass to prepare()lineHeight you pass to layout()If your component renders with a different font weight, font family, font size, or line height than the values you gave Pretext, your estimated result will drift from the actual DOM layout.
So keep the typography source of truth tight. If the component uses:
.message {
font:
400 16px Inter,
system-ui,
sans-serif;
line-height: 24px;
}then your measurement inputs should match those values.
This is the part worth being explicit about, because the interesting thing about Pretext is not that it replaces everything. It replaces one very specific pain point.
Pretext does not replace:
You still need a width from somewhere. Sometimes that is a prop. Sometimes it comes from your layout model. Sometimes a ResizeObserver is still appropriate. The difference is that you are no longer using the DOM to answer "how tall did this paragraph become?"
That is a much smaller and cleaner dependency on layout.
I would consider Pretext when all of these are true:
I would probably not reach for it when:
In other words, this is not "replace CSS with a library." It is "stop abusing the DOM as a calculator in text-heavy reactive UIs."
What makes Pretext interesting is not just the API. It is the shift in mental model.
For a long time, web developers mostly accepted that wrapped text measurement had to be a DOM problem. Pretext challenges that assumption. In a Vue app, that means some layout decisions that used to live in nextTick(), probe elements, and measurement loops can move back into pure reactive derivation.
That is exactly the kind of change I like: not because it is flashy, but because it removes a category of awkward code.
Pretext gives Vue developers a better option for text-heavy UI where height and line count matter. You prepare text once, lay it out against width as needed, and stop relying on hidden DOM nodes to tell you what wrapped text looks like.



Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.