# Show More

Long content clamped to a few lines, with a button to show the rest.

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

```tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { ShowMore, ShowMoreContent, ShowMoreTrigger } from "@/components/ui/show-more";
import { people } from "@/examples/data";

const comments = [
  {
    author: people[0],
    time: "2h",
    body: [
      "I reproduced the double charge on staging. When a card needs 3-D Secure, the confirm step retries on the client after the redirect, and the webhook also finalizes the invoice. Both paths call finalizeInvoice, so we bill twice whenever the webhook wins the race.",
      "The fix makes finalizing idempotent on the invoice ID and drops the client retry. It's in #2291, with a test that fires both paths within 50ms of each other. Refunds for the 14 customers affected go out tomorrow.",
    ],
  },
  {
    author: people[1],
    time: "1h",
    body: ["Approved. Ship it behind the billing flag first."],
  },
];

export default function ShowMoreComment() {
  return (
    <div className="flex w-full max-w-md flex-col gap-5">
      {comments.map((comment) => (
        <div key={comment.author.value} className="flex gap-3">
          <Avatar>
            <AvatarImage src={comment.author.image} alt="" />
            <AvatarFallback>{comment.author.initials}</AvatarFallback>
          </Avatar>
          <div className="flex min-w-0 flex-1 flex-col gap-1">
            <div className="flex items-baseline gap-2 text-sm">
              <span className="font-medium">{comment.author.name}</span>
              <span className="text-2xs text-faint">{comment.time}</span>
            </div>
            <ShowMore>
              <ShowMoreContent className="text-muted-foreground [&_p+p]:mt-2">
                {comment.body.map((paragraph) => (
                  <p key={paragraph}>{paragraph}</p>
                ))}
              </ShowMoreContent>
              <ShowMoreTrigger />
            </ShowMore>
          </div>
        </div>
      ))}
    </div>
  );
}
```

## 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/show-more
```

## Usage

```tsx
import { ShowMore, ShowMoreContent, ShowMoreTrigger } from "@/components/ui/show-more";

export function Comment({ body }: { body: string }) {
  return (
    <ShowMore lines={3}>
      <ShowMoreContent>
        <p>{body}</p>
      </ShowMoreContent>
      <ShowMoreTrigger />
    </ShowMore>
  );
}
```

`ShowMoreContent` shows the first `lines` lines, measured in its own line height, and fades out
the last. `ShowMoreTrigger` shows only when there's more to show, which is measured rather than
guessed, so short content gets neither the fade nor the button. Before the page loads in the
browser, CSS clamps the content to `lines` lines and fades the last of them, and keeps space for
the button, so long content already looks right in the server's render. Content shorter than
that gives the space back once it's measured.

Tabbing to a link in the hidden lines opens the content, so focus never lands on something
out of sight. Screen readers hear all of it, open or not.

## Composition

- `ShowMore`
  - `ShowMoreContent`
  - `ShowMoreTrigger`

## Examples

### Lines and labels

`lines` sets how many lines show while it's closed. Give `ShowMoreTrigger` your own words with
`more` and `less`.

```tsx
import { ShowMore, ShowMoreContent, ShowMoreTrigger } from "@/components/ui/show-more";

const changes = [
  "Plan changes are self-serve, and take effect at the next invoice.",
  "Metered usage is out of beta, with alerts at 80% and 100% of a limit.",
  "SSO comes with every paid plan, through Okta, Google or Azure.",
  "Audit logs export to your own S3 bucket every night.",
  "Invoices come as PDFs in nine languages.",
  "Webhooks retry with backoff for up to three days.",
];

export default function ShowMoreReleaseNotes() {
  return (
    <ShowMore lines={4} className="w-full max-w-sm">
      <h3 className="text-sm font-medium">What’s new in 4.2</h3>
      <ShowMoreContent className="text-muted-foreground">
        <ul className="list-disc ps-4">
          {changes.map((change) => (
            <li key={change}>{change}</li>
          ))}
        </ul>
      </ShowMoreContent>
      <ShowMoreTrigger more="All changes" less="Fewer changes" />
    </ShowMore>
  );
}
```

## Props

| Part | Extends |
| --- | --- |
| `ShowMore` | `<div>` |
| `ShowMoreContent` | `<div>` |
| `ShowMoreTrigger` | [`Button`](https://ui.jarv.is/components/button.md) |

### ShowMore

| Prop            | Type                        | Description                                                                                    |
| --------------- | --------------------------- | ---------------------------------------------------------------------------------------------- |
| `open?`         | `boolean`                   | Whether the content is shown in full, when it's controlled.                                    |
| `defaultOpen?`  | `boolean`                   | Whether the content is shown in full at first, when it isn't controlled. Default: `false`      |
| `onOpenChange?` | `((open: boolean) => void)` | Called when the trigger, or focus moving into the hidden lines, opens or closes it.            |
| `lines?`        | `number`                    | How many lines show while it's closed, measured in the content's own line height. Default: `3` |

### ShowMoreTrigger

| Prop                     | Type                                                                                 | Description                                                                                                                                                                                       |
| ------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nativeButton?`          | `boolean`                                                                            | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). Default: `true` |
| `focusableWhenDisabled?` | `boolean`                                                                            | Whether the button should be focusable when disabled. Default: `false`                                                                                                                            |
| `variant?`               | `"destructive" \| "ghost" \| "link" \| "outline" \| "primary" \| "secondary"`        | Default: `"ghost"`                                                                                                                                                                                |
| `size?`                  | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs"` | Default: `"sm"`                                                                                                                                                                                   |
| `more?`                  | `React.ReactNode`                                                                    | The label while the content is clamped. Default: `"Show more"`                                                                                                                                    |
| `less?`                  | `React.ReactNode`                                                                    | The label while the content is shown in full. Default: `"Show less"`                                                                                                                              |

`ShowMoreTrigger` takes the props of [Button](https://ui.jarv.is/components/button.md) too, and is a ghost button by
default.
