Truncate
Text shortened to fit its container — trailing off at the end, cut in the middle so both ends survive, or clamped to a few lines with a toggle. The tooltip appears only when text is actually hidden.
import { Truncate } from '@ontoui/react';
<Truncate.Root> <Truncate.Text>{filePath}</Truncate.Text> <Truncate.Tooltip /></Truncate.Root>;Anatomy
| Part | Description |
|---|---|
Truncate.Root | <span> — groups the parts and holds the mode and the state. |
Truncate.Text | <span> — the text. Shortened by CSS, and the tooltip’s trigger. |
Truncate.Tooltip | The full text, in a portal, and only while the text is cut. |
Truncate.Toggle | <button> — expands and collapses clamped text. |
Truncate.Text takes a string, not arbitrary nodes: middle mode has to split it, and the
tooltip has to repeat it.
Nothing is cut out of the DOM. All three modes leave the whole string in the markup and let CSS hide the overflow, so a screen reader reads the full value and a selection copies it — ellipsis and all.
Modes
End
The default. One line, trailing ellipsis, placed by the browser. Reach for it whenever the start of the value is the part that identifies it.
<Truncate.Root mode="end"> <Truncate.Text>{title}</Truncate.Text> <Truncate.Tooltip /></Truncate.Root>Middle
For values whose two ends both matter and whose middle does not — paths, hashes, wallet
addresses, signed URLs. endChars is how many characters are pinned at the tail; everything
before them takes the leftover width and ellipses into it.
<Truncate.Root mode="middle" endChars={16}> <Truncate.Text>{filePath}</Truncate.Text> <Truncate.Tooltip /></Truncate.Root>
<Truncate.Root mode="middle" endChars={6}> <Truncate.Text>{walletAddress}</Truncate.Text> <Truncate.Tooltip /></Truncate.Root>The split is where middle truncation is usually got wrong. Cutting the string at a character ratio assumes every character is the same width, which no proportional font makes true, and re-measuring the text in JavaScript on every resize is both expensive and a frame behind.
Instead the string is rendered as two segments in a row: the tail at its natural width, the
head taking whatever is left and ellipsing itself. The browser does the measuring it is
already doing for every other line of text on the page, so the cut lands in the right place
at any width, moves with a resize, survives a webfont swapping in — and, because both halves
are real text, a copy still yields the whole value rather than one with … in it.
Set endChars to the length of the part that has to survive — the file name, the last six
digits of an address. Count characters, not bytes; the split is code-point-aware, so an emoji
on the boundary stays whole. The tail never shrinks, so leave the container room for it: below
that width it is clipped rather than allowed to push into whatever sits next to it.
Clamp
For prose. lines is how many lines are shown; Truncate.Toggle reveals the rest.
<Truncate.Root mode="clamp" lines={2}> <Truncate.Text>{review}</Truncate.Text> <Truncate.Toggle /></Truncate.Root>Use a tooltip or a toggle, not both. A toggle is the better answer for a paragraph — a tooltip holding four lines of prose is a tooltip nobody can read.
The tooltip
A tooltip on text that is fully visible is noise, and a tab stop that reveals nothing is worse.
So Truncate.Text measures itself — scrollWidth against clientWidth, or scrollHeight
against clientHeight when clamped — and the tooltip stays switched off until that
measurement says something is hidden. The same measurement decides whether the text is
focusable at all, and whether Truncate.Toggle renders.
The measurement re-runs whenever the element resizes, whenever the text changes, and once more after webfonts finish loading, since the first reading ran against the fallback face.
Omitting Truncate.Tooltip turns the tooltip off entirely — and with it the tab stop, because
there would be nothing to reveal.
// Places the tooltip and gives it its own wording.<Truncate.Root> <Truncate.Text>{filePath}</Truncate.Text> <Truncate.Tooltip side="bottom">Open {fileName}</Truncate.Tooltip></Truncate.Root>The tooltip joins the delay group of an enclosing Tooltip.Provider, so a table of truncated
cells opens instantly after the first one instead of waiting out the delay on every cell.
<Tooltip.Provider delay={300}>{/* rows of Truncate */}</Tooltip.Provider>Layout
Truncate.Root is a <span> with display: block, so it is valid anywhere text is —
including inside a <p> — and it sizes itself from its container. There is nothing to
truncate against until something bounds its width: a fixed or maximum width, a table cell, or
a flex or grid track.
A flex or grid item is sized by its content unless told otherwise, so an ancestor between the
track and the text needs min-width: 0 for the text to be allowed to clip. Truncate.Root
already sets it on itself.
<div style={{ display: 'flex', gap: 8 }}> <Icon /> <Truncate.Root> <Truncate.Text>{filePath}</Truncate.Text> <Truncate.Tooltip /> </Truncate.Root></div>Accessibility
- The full text is always in the DOM. Truncation is visual, so a screen reader reads the whole value whether or not it fits. The tooltip repeats it for sighted users.
- Truncated text is a tab stop, and only then, so a keyboard user can bring up the tooltip.
Text that fits, or that has no
Truncate.Tooltip, stays out of the tab order. Truncate.Togglecarriesaria-expandedand pointsaria-controlsat the text it reveals. Give it a label in your own wording through its render prop when “Show more” is too vague to stand on its own in a list of them.- Nothing can be measured on the server. Under SSR the text renders unmeasured, so a toggle appears at hydration rather than in the first paint. Reserve its space in a layout where that shift would be disruptive.
API Reference
Truncate.Root
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
style? | CSSProperties | — |
mode? | "end" | "middle" | "clamp" | 'end' |
lines? | number | 3 |
endChars? | number | 8 |
expanded? | boolean | — |
defaultExpanded? | boolean | false |
onExpandedChange? | (expanded: boolean) => void | — |
ref? | Ref<HTMLSpanElement> | — |
Truncate.Text
| Prop | Type | Default |
|---|---|---|
children | string | — |
className? | string | — |
style? | CSSProperties | — |
ref? | Ref<HTMLSpanElement> | — |
Truncate.Tooltip
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | the full text |
className? | string | — |
side? | "top" | "right" | "bottom" | "left" | 'top' |
sideOffset? | number | 8 |
ref? | Ref<HTMLSpanElement> | — |
Truncate.Toggle
| Prop | Type | Default |
|---|---|---|
children? | (expanded: boolean) => ReactNode | (expanded) => (expanded ? 'Show less' : 'Show more') |
className? | string | — |
style? | CSSProperties | — |
ref? | Ref<HTMLSpanElement> | — |