Home / Blog / Using Pretext in Vue to Build Variable-Height UI Without Layout Thrash
Using Pretext in Vue to Build Variable-Height UI Without Layout Thrash

Using Pretext in Vue to Build Variable-Height UI Without Layout Thrash

Daniel Kelly
Daniel Kelly
Updated: April 3rd 2026

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:

  • hidden measurement nodes
  • getBoundingClientRect() and offsetHeight reads in hot paths
  • resize loops that mix layout and app state
  • virtualization code that needs a height before the row is even mounted

Pretext 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.

What Pretext actually does

The core model is simple:

  1. prepare(text, font) does the expensive work once.
  2. 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:

  • virtualized feeds
  • resizable sidebars
  • draggable canvases with labels
  • shrink-wrapped chat bubbles
  • card grids where item height depends on text

The Vue angle

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:

  • Vue owns the text, width, and rendering state.
  • Pretext derives height and line count from text plus width.
  • Your list, grid, or canvas logic consumes those numbers without mounting probe elements first.

That is a much cleaner separation of concerns.

Install it

ni @chenglou/pretext

A simple Vue composable

The 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?

  • Text changes should invalidate the prepared measurement.
  • Font changes should also invalidate it.
  • Width changes should only rerun layout.

That maps nicely onto Vue's computed graph.

Use it in a component

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.

Where this gets really useful

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.

1. Virtualized lists with variable-height text

Virtualization loves predictable heights. Text-heavy UIs often do not have them.

Pretext gives you a better story:

  • prepare message text when data arrives
  • compute row height from the current column width
  • feed that height into your virtualizer
  • rerun layout when the list width changes

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.

2. Chat bubbles that size to content

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:

  • deciding whether a bubble gets compact or roomy chrome
  • estimating whether a message should collapse behind "show more"
  • aligning metadata differently for one-line versus multi-line messages

Those are layout decisions based on text shape, not business logic. They should not require DOM probes in every component instance.

3. Canvas, whiteboards, and design tools

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:

  • drawing each wrapped line manually
  • finding the widest produced line
  • routing text line by line through changing widths

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.

A practical caveat: your font string has to match reality

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:

  • the font string you pass to prepare()
  • the 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.

What Pretext does not replace

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:

  • Vue rendering
  • CSS text styling
  • actual width measurement of your container
  • browser selection, caret behavior, or editing UX
  • the browser's final paint of the real text node

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.

When I would reach for it

I would consider Pretext when all of these are true:

  • text height or line count affects layout decisions
  • there are many text blocks, or the calculation happens often
  • hidden measurement DOM is making the code awkward or slow
  • you need the answer before mounting the final row or card

I would probably not reach for it when:

  • you are rendering a handful of static paragraphs
  • CSS alone solves the problem
  • you only need the browser to lay out the text once and never revisit it

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."

The broader idea

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.

Summary

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.

Start learning Vue.js for free

Daniel Kelly
Daniel Kelly
Daniel is the lead instructor at Vue School and enjoys helping other developers reach their full potential. He has 10+ years of developer experience using technologies including Vue.js, Nuxt.js, and Laravel.

Comments

Latest Vue School Articles

Generating Random IDs in Vue.js

Generating Random IDs in Vue.js

How Vue 3.5’s useId() composable gives you stable, unique DOM IDs for forms and accessibility—without manual counters or hydration bugs.
Daniel Kelly
Daniel Kelly
RAG with Nuxt and Gemini File Search

RAG with Nuxt and Gemini File Search

Build a practical, end-to-end RAG app with Nuxt and Gemini File Search. Index documents, retrieve grounded context, and answer user questions
Daniel Kelly
Daniel Kelly
VueSchool logo

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!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.