Truncated Text
Truncate text to one line or N lines with an ellipsis — and reveal the full text in a tooltip only when it actually overflows.
- Status
- Since
0.1.0- Accessibility pattern
- measured overflow disclosure
Last updated
Report name
Quarterly revenue reconciliation report — North America region, fiscal year 2026Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Truncated Text before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify truncated-textInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/truncated-textRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/tooltip, @vegastack/use-media-query, @vegastack/use-overflow.
Usage
import { TruncatedText } from "@/components/ui/truncated-text";
<div className="max-w-48">
<TruncatedText>{longTitle}</TruncatedText>
</div>;TruncatedText is purely presentational: it applies the truncation utility and,
when the text is actually clipped, wraps it in a Tooltip that surfaces the full
text on hover or keyboard focus. Width is owned by the parent — constrain the
surrounding box (a table cell, a max-w-* wrapper) and the text truncates to fit.
On devices that can't hover (phones, tablets — (hover: none)), Base UI's
Tooltip has no touch trigger, so clipped text would otherwise be permanently
unreadable. TruncatedText detects this and swaps in a tap-to-toggle
disclosure instead: a tap expands the text in place (it wraps instead of
clamping), and a second tap, Esc, or blur re-clamps it. Hover-capable
devices are unaffected, and keyboard focus keeps opening the Tooltip exactly as
before on every device.
Anatomy
Truncated Text is a compound component. Every exported part, with the
data-slot it renders (generated from the canonical source):
Examples
Single-line
The default — clamp to one line with a trailing ellipsis. Hover the clipped text to read it in full.
Report name
Quarterly revenue reconciliation report — North America region, fiscal year 2026Multi-line
Set lines greater than 1 to clamp to that many lines with CSS line-clamp
(supported up to 6). Use as="p" or as="div" for block-level copy.
Description
A long-form description that comfortably exceeds two lines inside a constrained column, so the text clamps with an ellipsis and the full copy is revealed in a tooltip on hover or keyboard focus.
Line scale
lines={1} truncates to a single line (truncate); 2–6 clamp to that many
lines (line-clamp-N). Values above 6 fall back to line-clamp-6. The same
overflowing copy is clamped at three line budgets below.
lines=1
A long-form description that comfortably exceeds two lines inside a constrained column, so the text clamps with an ellipsis and the full copy is revealed in a tooltip on hover or keyboard focus.
lines=3
A long-form description that comfortably exceeds two lines inside a constrained column, so the text clamps with an ellipsis and the full copy is revealed in a tooltip on hover or keyboard focus.
lines=6
A long-form description that comfortably exceeds two lines inside a constrained column, so the text clamps with an ellipsis and the full copy is revealed in a tooltip on hover or keyboard focus.
IconText
IconText lays out a fixed leading icon, a truncating label, and an optional
trailing slot (a badge, count, or kbd) on a single line. Only the label
truncates — the icon and trailing slot stay pinned — and the full label is
revealed in a Tooltip when it overflows. The common shape for sidebar items,
file rows, and nav labels.
import { IconText } from "@/components/ui/truncated-text";
import { Badge } from "@/components/ui/badge";
import { Icon } from "@vegastack/design/icons";
import { FileText } from "lucide-react";
<div className="max-w-48">
<IconText
icon={<Icon as={FileText} />}
text={fileName}
trailing={<Badge>New</Badge>}
tooltipSide="right"
/>
</div>;The icon is rendered decoratively (aria-hidden), so the label carries the
accessible name.
tooltipSide places the overflow tooltip on any edge of the row — top
(default), right, bottom, or left.
TableCellText
TableCellText is TruncatedText tuned for table cells: a width that matches
the column header so truncation engages predictably, plus an optional mono mode
for IDs and paths. It reuses the same overflow measurement and tooltip.
| Name | ID |
|---|---|
| Quarterly revenue reconciliation report — North America region, fiscal year 2026 | spc_3f9a17c4e2b84d6f9a01c5e7 |
| Marketing site | spc_a1 |
import { TableCellText } from '@/components/ui/truncated-text';
<TableCell>
<TableCellText text={space.name} width="200px" />
</TableCell>
<TableCell>
<TableCellText text={space.id} width="160px" mono />
</TableCell>;mono renders the value in the monospace family at the cell size (text-sm) — for IDs,
paths, and other fixed-width values. Set width to match the column header so
truncation engages within the column.
Inside tables and link rows
When text is clipped it becomes a Tooltip trigger and takes a tab stop, so keyboard users
can read it. In a 50-row table that is 50 extra tab stops on top of DataGrid's own roving
cell focus — which is why focusable exists.
It defaults to true standalone. A host that owns its own keyboard model flips the ambient
default to false by wrapping its content in TruncationFocusProvider. DataList and
DataGrid do exactly that around their tables, so a clipped value or a RelativeTime in a cell
is not a tab stop — Tab goes from one row's link straight to the next. Any single cell can opt
back in with focusable.
Under such a host, on a device that can hover, a value is measured only when the pointer reaches it: nothing at rest depends on whether it is clipped, so a grid that mounts hundreds of cells per scroll tick pays no layout read and no Tooltip per cell. The full text still opens on hover.
Nothing is hidden either way — CSS truncation is invisible to a screen reader, which reads the full string regardless; the tooltip only ever served sighted keyboard users. Geist and Linear do not make truncated cells focusable either. On a device that cannot hover the element stays focusable no matter what, because the tap-to-toggle disclosure is then the only way to read the text at all.
// A region that owns its own keyboard model turns the tab stops off
<TruncationFocusProvider focusable={false}>{rows}</TruncationFocusProvider>
// …and one cell can still opt back in
<TableCellText text={longNote} focusable />Playground
Change the line budget, tooltip side, and rendered element on an overflowing string, then copy the generated JSX.
<div className="max-w-48">
<TruncatedText>{longText}</TruncatedText>
</div>API Reference
TruncationFocusProvider
| Prop | Type | Default | Description |
|---|---|---|---|
focusable* | boolean | — | The focusable default for every TruncatedText / IconText /
RelativeTime beneath this provider. A component's own focusable prop
still wins. |
children | React.ReactNode | — | The region the default applies to — typically the rows of a grid or list. |
TruncatedText
| Prop | Type | Default | Description |
|---|---|---|---|
children* | React.ReactNode | — | The text to display. Truncated to lines and, when actually overflowing,
surfaced in full via a Tooltip on hover/focus. On devices that can't hover
((hover: none), e.g. touch phones/tablets) the same overflow instead makes
the element a tap-to-toggle disclosure — see the component doc. |
as | "div" | "p" | "span" | 'span' | The element to render. Single-line truncation needs a block-level box, so
p / div are also offered alongside the default inline span. |
focusable | boolean | — | Whether clipped text becomes a tab stop so keyboard users can open the
overflow Tooltip. Defaults to true standalone and to whatever a
TruncationFocusProvider sets — false under a grid or list host, whose
roving focus already owns cell reachability (decision D9).
Turning it off never hides content: CSS truncation is invisible to a screen
reader, which still reads the full string. On a device that cannot hover the
element stays focusable regardless, because the tap-to-toggle disclosure is
then the only way to read the text at all. |
lines | number | 1 | Number of lines to show before truncating with an ellipsis. 1 truncates
to a single line (truncate); >1 clamps to that many lines
(line-clamp-N, supported up to 6). |
tooltipSide | "bottom" | "left" | "right" | "top" | 'top' | Which side of the text to place the overflow Tooltip on. |
Data attributes and CSS variables on TruncatedText
| Attribute | Values |
|---|---|
data-lines | mirrors a prop or state value |
data-slot | "truncated-text" |
IconText
| Prop | Type | Default | Description |
|---|---|---|---|
icon* | React.ReactNode | — | Leading icon — render an Icon/lucide element (kept at its intrinsic size,
never shrunk). Tinted with text-muted-foreground to sit quietly beside the
label; pass your own color via the icon if you need emphasis. |
text* | string | — | The label text; truncated and surfaced in full via Tooltip when it overflows. |
focusable | boolean | — | Whether a clipped row becomes a tab stop. Same contract as
TruncatedText.focusable — see that prop. |
tooltipSide | "bottom" | "left" | "right" | "top" | 'top' | Which side to place the overflow Tooltip on. |
trailing | React.ReactNode | — | Optional trailing element (badge, count, kbd…), pinned and never truncated. |
Data attributes and CSS variables on IconText
| Attribute | Values |
|---|---|
data-slot | "icon-text" | "icon-text-label" |
TableCellText
| Prop | Type | Default | Description |
|---|---|---|---|
text* | string | — | The cell text; truncated and surfaced in full via Tooltip when it overflows. |
className | string | — | Extra classes for the text box. |
focusable | boolean | — | Whether a clipped cell becomes a tab stop. Same contract as
TruncatedText.focusable; under a grid or list host the ambient default is
already false. |
lines | number | 1 | Lines before clamping. 1 truncates to a single line; >1 clamps. |
mono | boolean | false | Render in the monospace family at the cell size (text-sm) — for IDs, paths, and other
fixed-width values. |
width | string | — | Cell width (match the column header), e.g. "200px". Constrains the box so
truncation engages within the column. |
Data attributes and CSS variables on TableCellText
| Attribute | Values |
|---|---|
data-slot | "table-cell-text" |
Accessibility
- Overflow is measured, not assumed — the disclosure (Tooltip or tap-toggle) only engages when the text is genuinely clipped, so screen-reader and keyboard users aren't offered an empty hint when the full text is already visible.
- When clipped, the text becomes a Tooltip trigger and opens on hover and
keyboard focus (never hover-only), so the full text is reachable without a
pointer. The popup carries
role="tooltip". focusablecontrols that tab stop; aTruncationFocusProviderturns it off for a whole region (the grid/list contract). This removes no information: the full string is always in the DOM and always read by assistive tech — the tooltip is a convenience for sighted keyboard users, and inside a grid the roving cell focus is the reachability model.- Touch fallback: Base UI's Tooltip is hover/focus-only — it has no touch
trigger — so on a
(hover: none)device the clipped element additionally becomes a tap-to-toggle disclosure:role="button"+aria-expandedtrack the expand/collapse state, a tap toggles it, and Enter/Space do the same for keyboard activation on hybrid devices. This never replaces the Tooltip — hover-capable devices and keyboard focus behave exactly as before. - The on-screen (truncated) text remains the element's text content, so the full string is still exposed to assistive technology through the trigger.
- A tooltip supplements text — it is not interactive. Keep links and controls out of the truncated content.
| Key | Action |
|---|---|
| Tab | Move focus to clipped text — its tooltip opens (when focusable). |
| Esc | Dismiss the open tooltip, or (touch fallback) re-clamp an expanded disclosure. |
| Enter / Space | (Touch fallback only) Toggle the expand/collapse disclosure. |
| Contract | States tested |
|---|---|
| Behaviour | default, active, expanded |
| Accessibility | expanded, keyboard |
| Visual | default, hover, active |