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.
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-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then 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.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/show-moreIts 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.13import { 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
Collapsibleinstead to hide a whole section behind its title. - Use
Paginationinstead 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. WithonShowMore, 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 takesitems.hasMore: Only withonShowMore.href: Only withonShowMore.lines: Text mode only: not withitems.loadedLabel: Only withonShowMore.loading: Only withonShowMore.loadingLabel: Only withonShowMore.noMoreLabel: Only withonShowMore.onShowMore: Only for a list that loads more: withitems, withoutinitialCountorlines.
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 withonShowMore.href: Only withonShowMore.initialCount: Only withitemsand noonShowMore.items: List modes only: not withlines.loadedLabel: Only withonShowMore.loading: Only withonShowMore.loadingLabel: Only withonShowMore.noMoreLabel: Only withonShowMore.onShowMore: Only for a list that loads more: withitems, withoutinitialCountorlines.ordered: List modes only: not withlines.
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
onShowMoreinstead. items(required)- Type:
readonly ReactNode[] - Every item, in order. Each renders in its own list item; give elements a
key. WithonShowMore, 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 takesitems.initialCount: Only withitemsand noonShowMore.lines: Text mode only: not withitems.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 usesinitialCount:<ShowMore items={items} initialCount={5}>.maxLines: Not a prop. Ouro UI useslines:<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
- layout-classes: test:registry/ouro/components/ouro/collapsible/show-more.test.tsx::ShowMore with items > lets app layout classes win and keys rows by their own keys
verify:testExists(registry/ouro/components/ouro/collapsible/show-more.test.tsx::ShowMore with items > lets app layout classes win and keys rows by their own keys)