# Truncate

A line of text that ends in an ellipsis when it doesn't fit, with the rest in a tooltip.

Source: [truncate.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/truncate.tsx)

```tsx
import { FileArchiveIcon, FileImageIcon, FileTextIcon } from "lucide-react";

import { Truncate } from "@/components/ui/truncate";

const files = [
  { name: "q3-board-update-final-v4-with-appendix.pdf", size: "2.4 MB", icon: FileTextIcon },
  { name: "homepage-hero-illustration-dark@2x.png", size: "860 KB", icon: FileImageIcon },
  { name: "brand-assets.zip", size: "48 MB", icon: FileArchiveIcon },
  { name: "customer-interview-transcripts-batch-2.docx", size: "312 KB", icon: FileTextIcon },
];

export default function TruncateFileList() {
  return (
    <ul className="flex w-full max-w-64 flex-col">
      {files.map(({ name, size, icon: Icon }) => (
        <li key={name} className="flex items-center gap-2 py-1.5 text-sm">
          <Icon className="size-4 shrink-0 text-icon-muted" />
          <Truncate position="middle" className="flex-1">
            {name}
          </Truncate>
          <span className="shrink-0 text-xs text-faint tabular-nums">{size}</span>
        </li>
      ))}
    </ul>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/truncate
```

## Usage

```tsx
import { Truncate } from "@/components/ui/truncate";

export function FileName({ name }: { name: string }) {
  return <Truncate position="middle">{name}</Truncate>;
}
```

`Truncate` keeps its text on one line and cuts it off with an ellipsis once it's wider than the
space it has. Hovering the text shows all of it in a tooltip, and so does tapping it on a touch
screen or tabbing to it, but only while it's truncated. Inside a link, a button or anything else
that takes focus, like a [tree](https://ui.jarv.is/components/tree.md) item, a tap and the tab key belong to
that, so only hovering shows the tooltip. A scrolling list doesn't count, even one that takes
focus so it can be scrolled from the keyboard. The text is measured again whenever its space or
its own width changes, so resizing a panel or making the text bold brings back whatever fits.

It's a block, so give it a width the way you would any block, like `flex-1` in a row. It takes
a string, not other elements.

## Examples

### Positions

The ellipsis goes at the end by default. `position="middle"` keeps the start and end of the
text and cuts out the middle instead, for file names, paths, hashes and URLs, which differ in
their last few characters. Before the page loads in the browser, the middle cut shows as an
ellipsis at the end. Screen readers hear the whole text either way, and copying it copies all of
it, without the ellipsis.

```tsx
import { Truncate } from "@/components/ui/truncate";

const path = "~/projects/website/src/components/marketing/pricing-table.tsx";

export default function TruncatePositions() {
  return (
    <div className="flex w-full max-w-56 flex-col gap-3 text-sm">
      <div className="flex flex-col gap-1">
        <span className="text-xs text-faint">End</span>
        <Truncate>{path}</Truncate>
      </div>
      <div className="flex flex-col gap-1">
        <span className="text-xs text-faint">Middle</span>
        <Truncate position="middle">{path}</Truncate>
      </div>
    </div>
  );
}
```

### In a table

A table cell grows to fit its text unless the table has a fixed layout. Add `table-fixed` to the
table and give the columns that don't truncate a width.

Each truncated text is a tab stop of its own, so a long table or list of them takes many presses
of the tab key to get through. Pass `tabIndex={-1}` to leave them out of the tab order. Hovering
and tapping still show the tooltip, and screen readers still hear the whole text.

```tsx
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { Truncate } from "@/components/ui/truncate";

const commits = [
  {
    sha: "9f2c41e7b0d3a85c6e14f2b9d07a3c58e61b4f20",
    message: "Make finalizing invoices idempotent so the webhook and the client can't both bill",
    author: "Ada Park",
  },
  {
    sha: "4b7e0a19c3f25d86e0b1a47c9d2f3e58a06b7c14",
    message: "Drop the client retry after 3-D Secure",
    author: "Sam Rivera",
  },
  {
    sha: "e03d58b2a6c94f17d2e0b83a5c16f4d97b2e0a63",
    message: "Test that both paths firing within 50ms bill once",
    author: "Ada Park",
  },
];

export default function TruncateTable() {
  return (
    <Table className="table-fixed">
      <TableHeader>
        <TableRow>
          <TableHead className="w-24">Commit</TableHead>
          <TableHead>Message</TableHead>
          <TableHead className="w-28">Author</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {commits.map((commit) => (
          <TableRow key={commit.sha}>
            <TableCell className="font-mono text-xs">
              <Truncate position="middle">{commit.sha}</Truncate>
            </TableCell>
            <TableCell>
              <Truncate>{commit.message}</Truncate>
            </TableCell>
            <TableCell>
              <Truncate>{commit.author}</Truncate>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
```

### Without a tooltip

`tooltip={false}` leaves the tooltip out, for text whose whole version is a click away.

```tsx
import { Truncate } from "@/components/ui/truncate";

export default function TruncateWithoutTooltip() {
  return (
    <a
      href="#billing-webhooks"
      className="flex w-full max-w-60 flex-col gap-0.5 rounded-md p-2 text-sm hover:bg-hover"
    >
      <Truncate tooltip={false} className="font-medium">
        Billing webhooks retry with backoff for up to three days
      </Truncate>
      <Truncate tooltip={false} position="middle" className="text-xs text-muted-foreground">
        https://docs.example.com/billing/webhooks/retries-and-backoff
      </Truncate>
    </a>
  );
}
```

## Props

| Part | Extends |
| --- | --- |
| `Truncate` | `<span>` |

### Truncate

| Prop        | Type                | Description                                                            |
| ----------- | ------------------- | ---------------------------------------------------------------------- |
| `children`  | `string`            | The text, which stays on one line.                                     |
| `position?` | `"end" \| "middle"` | Where the ellipsis goes when the text doesn't fit. Default: `"end"`    |
| `tooltip?`  | `boolean`           | Shows the full text in a tooltip while it's truncated. Default: `true` |
