Lists

ShowMore

"Show more" for a long list or a long text. It cuts a list to its first items, clamps text to a number of lines, or grows a list a page at a time from the server, and moves focus to the first newly shown item, so keyboard and screen reader users land on what they asked for. A list that loads more also says how many items now show. The visible items are in the server HTML. ShowMore draws the list itself: put a border around the list on a wrapping element, and borders between rows on each item.

Brown the short ribs in batches, then soften onions, carrots and celery in the fat. Deglaze with red wine, cover with stock and braise for three hours, until the meat falls from the bone.

  • Braised short ribs

  • Lemon ricotta pancakes

  • Weeknight dal

In Spoonjoy

Spoonjoy, a recipe app, is built with Ouro HX (how).

See it live, no sign-in needed: A recipe · Recipes

In its source: app/components/recipe/SpoonsStrip.tsx · app/components/pantry/RecipeGrid.tsx · app/routes/recipes._index.tsx

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/show-more#v1.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/show-more

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/show-more#v1.2.13 ourostack/ouro-ui/show-more-tests#v1.2.13
import { ShowMore } from "@/components/ouro/collapsible/show-more";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<ShowMore items={reviews.map((review) => <Review key={review.id} {...review} />)} initialCount={3} />
<ShowMore lines={3}>{recipe.description}</ShowMore>
<ShowMore items={rows} hasMore={Boolean(nextCursor)} loading={loading} onShowMore={loadNext} href={nextHref} />

Use instead when

  • Use Collapsible instead to hide a whole section behind its title.
  • Use Pagination instead when people need to jump to a page or share its URL.

Props

The props the modes share. Type: ShowMoreProps.

className
Type: string
Layout only: margins, width, grid placement (mt-6). Text classes go on a parent so the items inherit them.
ref
Type: Ref<HTMLDivElement>
Reaches the rendered element.
showMoreLabel
Type: string
Default: "Show more"
The button's text while there is more to show. Translate it here.

Modes

ShowMoreProps is a union: pass the props of exactly one mode. Mixing them is a type error.

Items: ShowMoreItemsProps

A list cut to its first initialCount items, with "Show more" and "Show less".

initialCount
Type: number
Default: 5
How many items show while collapsed.
items (required)
Type: readonly ReactNode[]
Every item, in order. Each renders in its own list item; give elements a key. With onShowMore, append each new page here.
ordered
Type: boolean
Default: false (<ul>)
Renders an <ol>, for a ranked or numbered list.
showLessLabel
Type: string
Default: "Show less"
The button's text while everything shows. Translate it here.

Not in this mode (typed never):

  • children: Text mode only: a list takes items.
  • hasMore: Only with onShowMore.
  • href: Only with onShowMore.
  • lines: Text mode only: not with items.
  • loadedLabel: Only with onShowMore.
  • loading: Only with onShowMore.
  • loadingLabel: Only with onShowMore.
  • noMoreLabel: Only with onShowMore.
  • onShowMore: Only for a list that loads more: with items, without initialCount or lines.

Lines: ShowMoreLinesProps

Long text clamped to lines lines, with "Show more" and "Show less".

children (required)
Type: ReactNode
The text. Screen readers always read all of it.
lines (required)
Type: number
How many lines show while collapsed. The button appears only when the text is longer.
showLessLabel
Type: string
Default: "Show less"
The button's text while everything shows. Translate it here.

Not in this mode (typed never):

  • hasMore: Only with onShowMore.
  • href: Only with onShowMore.
  • initialCount: Only with items and no onShowMore.
  • items: List modes only: not with lines.
  • loadedLabel: Only with onShowMore.
  • loading: Only with onShowMore.
  • loadingLabel: Only with onShowMore.
  • noMoreLabel: Only with onShowMore.
  • onShowMore: Only for a list that loads more: with items, without initialCount or lines.
  • ordered: List modes only: not with lines.

Load: ShowMoreLoadProps

A list the server grows a page at a time: "Show more" asks for the next page and appends it.

hasMore (required)
Type: boolean
Whether there is another page. Without one, the button goes away.
href
Type: string
The next page's URL, so the button is a link that works before JavaScript loads. With JavaScript, pressing it calls onShowMore instead.
items (required)
Type: readonly ReactNode[]
Every item, in order. Each renders in its own list item; give elements a key. With onShowMore, append each new page here.
loadedLabel
Type: ((count: number) => string)
Default: (count) => Showing ${count} items
What screen readers hear after each page, from the number of items now shown. Name the items and translate it here: (count) => \Showing ${count} recipes\``.
loading
Type: boolean
Shows the button as busy, and keeps it from asking twice.
loadingLabel
Type: string
Default: "Loading"
What screen readers hear when loading starts. Translate it here.
noMoreLabel
Type: string
Default: "No more items"
What screen readers hear when a page comes back empty and there is no more. Translate it here.
onShowMore (required)
Type: () => void
Asks for the next page. The button is busy while loading, and focus moves to the first new item when it arrives.
ordered
Type: boolean
Default: false (<ul>)
Renders an <ol>, for a ranked or numbered list.

Not in this mode (typed never):

  • children: Text mode only: a list takes items.
  • initialCount: Only with items and no onShowMore.
  • lines: Text mode only: not with items.
  • showLessLabel: A list that loads more never shrinks, so it has no "Show less".

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • limit: Not a prop. Ouro UI uses initialCount: <ShowMore items={items} initialCount={5}>.
  • maxLines: Not a prop. Ouro UI uses lines: <ShowMore lines={3}>.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: ShowMore merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.

Claim sources