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
Add Truncated Text from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/truncated-textThe same command installs the registry items it composes: @vegastack/tooltip, @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 a smaller size — for IDs,
paths, and other fixed-width values. Set width to match the column header so
truncation engages within the column.
Focus inside a grid or list
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 — that is the contract
DataList and DataGrid adopt, since their roving cell focus already owns reachability. Any
single cell can opt back in with focusable.
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 a smaller size — 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 |