vue-clamp
Clamping primitives for Vue 3. The default components measure real browser layout so text, inline content, and wrapped items fit the space they are actually rendered into. A separate predictive text entry is available for controlled, high-frequency resize workloads.
- Live docs and demos: vue-clamp.void.app
- Migration guide: MIGRATION.md
- Release notes: CHANGELOG.md
Install
pnpm add vue-clamp
vue-clamp has a peer dependency on Vue ^3.5.0. Install Vue too if your project does not already
depend on it.
Components
| Component | Use it for |
|---|---|
<LineClamp> |
Multiline plain text with optional start, middle, or end ellipsis. |
<RichLineClamp> |
Trusted inline HTML that should keep formatting while clamping. |
<InlineClamp> |
One-line strings with fixed affixes and configurable ellipsis. |
<WrapClamp> |
Wrapped atomic items such as tags, filters, chips, and breadcrumbs. |
The package has named exports only:
import { InlineClamp, LineClamp, RichLineClamp, WrapClamp } from "vue-clamp";
The predictive component is an explicit subpath import:
import { LineClamp } from "vue-clamp/pretext";
Quick start
<script setup lang="ts">
import { ref } from "vue";
import { LineClamp } from "vue-clamp";
const expanded = ref(false);
const text = "Ship review-ready notes with browser-fit text truncation and keep the toggle inline.";
</script>
<template>
<LineClamp v-model:expanded="expanded" :text="text" :max-lines="2">
<template #after="{ clamped, expanded, toggle }">
<button v-if="clamped" type="button" @click="toggle">
{{ expanded ? "Less" : "More" }}
</button>
</template>
</LineClamp>
</template>
Plain text
Use <LineClamp> when the source is plain text and the browser should decide line wrapping.
<LineClamp :text="title" :max-lines="2" location="middle" boundary="word" ellipsis="..." />
Useful props:
text: source text. Defaults to"".max-lines: maximum visible line count.max-height: maximum visible height. Numbers are treated as pixels.ellipsis: string inserted into clamped output. Defaults to….location:start,middle,end, or a number from0to1. Defaults toend.boundary:graphemeorword. Defaults tographeme. Usewordto avoid partial words; single-line word-boundary clamping uses the measured JS path instead of nativetext-overflow.expanded: show the full text. Supportsv-model:expanded.
before and after slots render inline with the text and receive
{ expand, collapse, toggle, clamped, expanded }.
Predictive plain text
Keep the root entry by default. Choose vue-clamp/pretext when:
- many mounted plain-text clamps resize repeatedly;
- they use
boundary="word", end truncation, andmax-lineswithoutmax-height; and - typography is stable, a named font is loaded, and exact browser-only shaping is not required.
One-off clamps, native-eligible grapheme clamps, and measured fallbacks do not amortize the approximately 20 kB gzip predictor payload.
<script setup lang="ts">
import { LineClamp } from "vue-clamp/pretext";
</script>
<template>
<LineClamp class="title" :text="title" :max-lines="2" boundary="word" />
</template>
<style>
.title {
font:
16px Inter,
sans-serif;
}
</style>
The subpath has the same public API and selects the cheapest compatible engine:
- Native CSS for the standard default end/grapheme/
…subset. - Pretext for word-boundary end truncation with
max-linesand nomax-height, including observedbeforeandafterwidths and custom ellipses without forced line breaks. - The standard browser-measured engine for every other combination. Supported non-native
end/grapheme clamping with
max-linesand nomax-heightrefines across word transitions and within the last word, including a multilineafterslot or a custom ellipsis. Both entries use the same measured cut points.
All modes share the standard DOM, accessibility, controls, events, and observation runtime. Before
prediction, the component caches the rendered font plus Pretext-supported white-space,
word-break, and numeric letter-spacing. Other CSS still renders but is outside the model, so
platform font metrics and features such as automatic hyphenation, contextual spacing, font features,
or dynamic typography can produce a conservative shorter prefix. CSS line clamping and overflow containment prevent extra
lines from painting. Pretext also documents system-ui as unsafe on macOS.
After preparation, eligible word-boundary resizes perform no DOM geometry or computed-style reads. Prefer the root entry whenever full browser CSS fidelity matters more than repeated-resize throughput.
Trusted rich text
Use <RichLineClamp> for trusted or already-sanitized inline markup.
<RichLineClamp v-model:expanded="expanded" :html="html" :max-lines="2">
<template #after="{ clamped, expanded, toggle }">
<button v-if="clamped" type="button" @click="toggle">
{{ expanded ? "Less" : "More" }}
</button>
</template>
</RichLineClamp>
Rich clamping is intentionally scoped:
htmlis rendered as HTML. Sanitize untrusted input before passing it in.- Rich content clamps from the end only.
boundarycan begraphemeorword. Defaults tographeme;wordavoids partial words inside supported text runs.- Supported default-ellipsis
max-linescases use native CSS clamping. The authored rich DOM stays intact and the ellipsis is visual rather than an inserted text node. - Passive inline elements can participate when they can be cloned back into the DOM and stay in inline flow.
- Empty elements without light DOM content are treated as atomic inline units.
br,wbr,img, and outersvgelements have explicit handling when they stay in inline flow.- Inline rich images must have deterministic rendered dimensions before loading, set by attributes or CSS.
- When custom behavior requires measured clamping, custom elements, duplicate IDs or named form controls, active embedded content, and inline event handlers fall back to the original HTML because connected probe clones would not be reliably inert.
- Markup unsupported by the measured inline-flow model falls back to the original HTML unchanged.
before and after slots receive the same control props as <LineClamp>.
Single-line strings
Use <InlineClamp> for one-line text where part of the string should remain fixed while the body
shrinks. The location prop controls how body text is kept around the ellipsis; in tight spaces,
the body can become just the ellipsis.
<script setup lang="ts">
import { InlineClamp } from "vue-clamp";
const file = "summer-campaign-panorama-final.jpeg";
function splitFileName(text: string) {
const extension = text.match(/\.[^.]+$/)?.[0];
return extension ? { body: text.slice(0, -extension.length), end: extension } : { body: text };
}
</script>
<template>
<InlineClamp :text="file" :split="splitFileName" location="middle" />
</template>
The default unsplit location="end", boundary="grapheme", and ellipsis="…" combination keeps
the full source text in the DOM and uses native CSS overflow. split, start/middle truncation,
word boundaries, and custom ellipsis strings use live browser measurement so their exact semantics
are preserved.
Useful props:
text: required source string.ellipsis: string inserted into the rewritten body. Defaults to….location: how body text is kept around the ellipsis:start,middle,end, or a number from0to1. Defaults toend.boundary:graphemeorword. Defaults tographeme;wordavoids partial words in the rewritten body, falling back to grapheme cuts when no whole word can fit.split: optional function returning{ start?: string, body: string, end?: string }.as: root tag name. Defaults tospan.
<InlineClamp> has no slots or expansion API.
Wrapped items
Use <WrapClamp> when each item must stay whole.
<script setup lang="ts">
import { ref } from "vue";
import { WrapClamp } from "vue-clamp";
const expanded = ref(false);
const labels = [
{ id: "perf", label: "Performance" },
{ id: "a11y", label: "Accessibility" },
{ id: "docs", label: "Docs" },
{ id: "qa", label: "Needs QA" },
];
</script>
<template>
<WrapClamp v-model:expanded="expanded" :items="labels" item-key="id" :max-lines="2">
<template #item="{ item }">
<span class="tag">{{ item.label }}</span>
</template>
<template #after="{ clamped, expanded, hiddenItems, toggle }">
<button v-if="expanded || clamped" type="button" @click="toggle">
{{ expanded ? "Less" : `+${hiddenItems.length} more` }}
</button>
</template>
</WrapClamp>
</template>
Useful props:
items: ordered source items. Defaults to[].item-key: string field name or(item, index) => string | numberkey resolver.max-lines: maximum visible wrapped line count.max-height: maximum visible height. Numbers are treated as pixels.expanded: show the full item list. Supportsv-model:expanded.
The required item slot receives { item, index }. The before and after slots receive
{ expand, collapse, toggle, clamped, expanded, hiddenItems }.
Events and instance methods
Both <LineClamp> variants, <RichLineClamp>, and <WrapClamp> emit:
clampchange:(clamped: boolean), emitted when truncation turns on or off.update:expanded:(expanded: boolean), emitted forv-model:expanded.
They also expose expand(), collapse(), toggle(), clamped, and expanded through a template
ref.
Styling hooks
Stable styling hooks use data-part attributes:
| Component | Parts |
|---|---|
<LineClamp> |
root, content, before, body, after |
<RichLineClamp> |
root, content, before, body, after |
<InlineClamp> |
root, start, body, end |
<WrapClamp> |
root, content, before, item, after |
Do not rely on internal DOM nesting as a styling contract.
The Pretext entry uses the same parts on native, predictive, and browser-measured paths.
Notes
1.xis the Vue 3 line. See the migration guide when upgrading from0.x.ResizeObserveris part of the browser baseline.- Multiline native clamping uses the specified legacy
-webkit-line-clampcombination. The unprefixedline-clampproperty is not required.