Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

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
stable
Since
0.1.0
Accessibility pattern
measured overflow disclosure

Last updated

Report name

Quarterly revenue reconciliation report — North America region, fiscal year 2026

Install

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-text

The 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):

IconText — data-slot="icon-text" | "icon-text-label"
TableCellText — data-slot="table-cell-text"
TruncatedText — data-slot="truncated-text"
TruncationFocusProvider

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 2026

Multi-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); 26 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.

Q3-2026-revenue-reconciliation-north-america-final-v7.xlsxNew
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.

tooltipSide="top" — Q3-2026-revenue-reconciliation-north-america-final-v7.xlsx
tooltipSide="right" — Q3-2026-revenue-reconciliation-north-america-final-v7.xlsx
tooltipSide="bottom" — Q3-2026-revenue-reconciliation-north-america-final-v7.xlsx
tooltipSide="left" — Q3-2026-revenue-reconciliation-north-america-final-v7.xlsx

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.

NameID
Quarterly revenue reconciliation report — North America region, fiscal year 2026spc_3f9a17c4e2b84d6f9a01c5e7
Marketing sitespc_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.

Quarterly infrastructure migration retrospective — capacity planning, on-call rotation, and budget notes for the platform team
<div className="max-w-48">
  <TruncatedText>{longText}</TruncatedText>
</div>

API Reference

TruncationFocusProvider

PropTypeDefaultDescription
focusable*booleanThe focusable default for every TruncatedText / IconText / RelativeTime beneath this provider. A component's own focusable prop still wins.
childrenReact.ReactNodeThe region the default applies to — typically the rows of a grid or list.

TruncatedText

PropTypeDefaultDescription
children*React.ReactNodeThe 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.
focusablebooleanWhether 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.
linesnumber1Number 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

AttributeValues
data-linesmirrors a prop or state value
data-slot"truncated-text"

IconText

PropTypeDefaultDescription
icon*React.ReactNodeLeading 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*stringThe label text; truncated and surfaced in full via Tooltip when it overflows.
focusablebooleanWhether 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.
trailingReact.ReactNodeOptional trailing element (badge, count, kbd…), pinned and never truncated.

Data attributes and CSS variables on IconText

AttributeValues
data-slot"icon-text" | "icon-text-label"

TableCellText

PropTypeDefaultDescription
text*stringThe cell text; truncated and surfaced in full via Tooltip when it overflows.
classNamestringExtra classes for the text box.
focusablebooleanWhether a clipped cell becomes a tab stop. Same contract as TruncatedText.focusable; under a grid or list host the ambient default is already false.
linesnumber1Lines before clamping. 1 truncates to a single line; >1 clamps.
monobooleanfalseRender in the monospace family at a smaller size — for IDs, paths, and other fixed-width values.
widthstringCell width (match the column header), e.g. "200px". Constrains the box so truncation engages within the column.

Data attributes and CSS variables on TableCellText

AttributeValues
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".
  • focusable controls that tab stop; a TruncationFocusProvider turns 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-expanded track 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.
KeyAction
TabMove focus to clipped text — its tooltip opens (when focusable).
EscDismiss the open tooltip, or (touch fallback) re-clamp an expanded disclosure.
Enter / Space(Touch fallback only) Toggle the expand/collapse disclosure.
ContractStates tested
Behaviourdefault, active, expanded
Accessibilityexpanded, keyboard
Visualdefault, hover, active

Do / Don't

Do
Constrain the parent (a cell or max-w wrapper) and let TruncatedText clamp the text to fit — the full value stays one hover away.
Don't
Truncate essential, action-bearing text behind a tooltip — a tooltip isn't reliably reachable on touch; show the full value or use a different layout.

On this page