# Truncate Without Losing the Value

**MUST** · **ID:** `content-truncation-keeps-value` · **Category:** content
**Source:** [jakubkrehel](https://github.com/jakubkrehel/skills)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/content-truncation-keeps-value

> MUST: If the text an ellipsis hides discriminates rather than decorates, keep the full value reachable — a `title` (which also lands in the accessible name), a tooltip, an expanded view, or middle truncation that pins the discriminating tail (`invoice-2026-…-final-v2.pdf`) and collapses the boring head. Hover-only tooltips are not enough on their own: keyboard and touch users never trigger them, so pair them with something focusable.

If the text an ellipsis hides carries meaning, keep the full value reachable in a tooltip or an expanded view

Two neighbouring rules already cover the mechanics and neither covers this. content-ibelick-text-overflow says to USE `truncate` or `line-clamp` in dense UI; layout-min-width-truncation makes truncation actually WORK inside a flex or grid child. Both are about the ellipsis appearing where it should. This rule is about what the ellipsis ate. A file column that renders `invoice-2026-q3-a…` on every row has not shortened three filenames, it has deleted the only part that told them apart, and the user is left choosing an invoice by its byte count. So: decide whether the hidden text is decoration or discrimination. If it discriminates, keep it reachable — a `title` (which also lands in the accessible name), a tooltip, a details view, or middle truncation that pins the tail (`invoice-2026-…-final-v2.pdf`) and collapses the boring head instead. Hover-only tooltips are not enough on their own, since keyboard and touch users never trigger them, so pair them with something focusable or an expand toggle.

## Bad — do not do this

`content-truncation-keeps-value-bad`

```tsx
/**
 * Bad: the column truncates cleanly — and every row truncates to the same string.
 *
 * `truncate` did its job: nothing overflows, the grid holds. But the part of the
 * name that told the rows apart lived in the tail, and the tail is gone. The user
 * cannot pick the right invoice, cannot tell the draft from the final, and has no
 * way to recover the value: no tooltip, no expanded view, no full name anywhere.
 */

const FILES = [
  { name: 'invoice-2026-q3-acme-holdings-final-v2.pdf', size: '184 KB', date: 'Jul 2' },
  { name: 'invoice-2026-q3-acme-holdings-draft.pdf', size: '181 KB', date: 'Jun 28' },
  { name: 'invoice-2026-q3-acme-logistics-final.pdf', size: '203 KB', date: 'Jun 24' },
];

export function TruncationKeepsValueBad() {
  return (
    <div className="w-full space-y-3">
      <div className="overflow-hidden rounded-lg border border-border bg-card">
        <table className="w-full table-fixed text-sm">
          <thead>
            <tr className="border-b border-border text-left text-xs text-muted-foreground">
              <th scope="col" className="w-1/2 px-3 py-2 font-medium">
                Name
              </th>
              <th scope="col" className="px-3 py-2 font-medium">
                Size
              </th>
              <th scope="col" className="px-3 py-2 font-medium">
                Modified
              </th>
            </tr>
          </thead>
          <tbody>
            {FILES.map((file) => (
              <tr key={file.name} className="border-b border-border last:border-0">
                {/* No title, no tooltip, no expanded view — the tail is simply unreachable */}
                <td className="truncate px-3 py-2 text-foreground">{file.name}</td>
                <td className="px-3 py-2 tabular-nums text-muted-foreground">{file.size}</td>
                <td className="px-3 py-2 text-muted-foreground">{file.date}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <p className="text-xs text-destructive">
        Three different files, one visible name: <code>invoice-2026-q3-a…</code>. The only thing
        left to choose by is the file size. Truncation that hides the discriminating part of a value
        and offers no way back to it has not shortened the content — it has deleted it.
      </p>
    </div>
  );
}
```

## Good — do this

`content-truncation-keeps-value-good`

```tsx
import { useState } from 'react';

/**
 * Good: same column width, same truncation — but the value stays reachable.
 *
 * 1. Middle-truncate instead of tail-truncate. The head goes under the ellipsis and
 *    the discriminating tail (`…-final-v2.pdf`) survives, so the rows stay tellable
 *    apart at a glance.
 * 2. Keep the full string retrievable anyway: `title` gives it on hover and to the
 *    accessible name, and the toggle expands the column for keyboard and touch users,
 *    who never get a hover tooltip.
 */

const FILES = [
  { name: 'invoice-2026-q3-acme-holdings-final-v2.pdf', size: '184 KB', date: 'Jul 2' },
  { name: 'invoice-2026-q3-acme-holdings-draft.pdf', size: '181 KB', date: 'Jun 28' },
  { name: 'invoice-2026-q3-acme-logistics-final.pdf', size: '203 KB', date: 'Jun 24' },
];

const TAIL_LENGTH = 16;

export function TruncationKeepsValueGood() {
  const [expanded, setExpanded] = useState(false);

  return (
    <div className="w-full space-y-3">
      <div className="overflow-hidden rounded-lg border border-border bg-card">
        <table className="w-full table-fixed text-sm">
          <thead>
            <tr className="border-b border-border text-left text-xs text-muted-foreground">
              <th scope="col" className="w-1/2 px-3 py-2 font-medium">
                Name
              </th>
              <th scope="col" className="px-3 py-2 font-medium">
                Size
              </th>
              <th scope="col" className="px-3 py-2 font-medium">
                Modified
              </th>
            </tr>
          </thead>
          <tbody>
            {FILES.map((file) => {
              const head = file.name.slice(0, -TAIL_LENGTH);
              const tail = file.name.slice(-TAIL_LENGTH);

              return (
                <tr key={file.name} className="border-b border-border last:border-0">
                  <td className="px-3 py-2 text-foreground">
                    {expanded ? (
                      <span className="break-all">{file.name}</span>
                    ) : (
                      /* Middle truncation: the head collapses under the ellipsis,
                         the tail is pinned and never clipped. */
                      <span title={file.name} className="flex min-w-0">
                        <span className="truncate">{head}</span>
                        <span className="shrink-0">{tail}</span>
                      </span>
                    )}
                  </td>
                  <td className="px-3 py-2 tabular-nums text-muted-foreground">{file.size}</td>
                  <td className="px-3 py-2 text-muted-foreground">{file.date}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

      <button
        type="button"
        onClick={() => setExpanded((value) => !value)}
        aria-pressed={expanded}
        className="rounded-md border border-border bg-card px-3 py-1.5 text-xs font-medium text-foreground hover:bg-muted focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        {expanded ? 'Truncate names' : 'Show full names'}
      </button>

      <p className="text-xs text-success">
        <code>…-holdings-final-v2.pdf</code> and <code>…-holdings-draft.pdf</code> are now two
        different rows. Truncation is a display choice; the value it hides has to stay retrievable —
        by tooltip, by accessible name, or by an expanded view.
      </p>
    </div>
  );
}
```

## References

- [jakubkrehel — better-typography SKILL.md](https://raw.githubusercontent.com/jakubkrehel/skills/main/skills/better-typography/SKILL.md)
- [jakubkrehel — wrapping-and-punctuation.md](https://raw.githubusercontent.com/jakubkrehel/skills/main/skills/better-typography/wrapping-and-punctuation.md)
- [MDN — text-overflow](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/text-overflow)
- [MDN — title attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/title)
