Lists
Item
One row of a list: leading media, a title with a description, and trailing actions. With href the whole row is one link, named by its title, with one tab stop, and the actions stay their own. Put rows in an ItemGroup, which makes them a list.
In Spoonjoy
Spoonjoy, a recipe app, is built with Ouro HX (how).
See it live, no sign-in needed: A recipe · Recipes · Search
In its source: app/routes/my-recipes.tsx · app/components/recipe/SpoonsStrip.tsx · app/components/users/FellowChefList.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/item#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/itemIts 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/item#v1.2.13 ourostack/ouro-ui/item-tests#v1.2.13import { Item } from "@/components/ouro/item/item";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<Item
href="/recipes/short-ribs"
media={<img src={photo} alt="" />}
mediaSize="md"
title="Braised short ribs"
description="Four hours, mostly hands-off"
/>
<Item title="Passkey on iPhone" description="Added May 2" actions={<Button variant="plain">Remove</Button>} stackBelow="sm" />Use instead when
- Use
Cardinstead for a larger tile with a cover image and a body, andRowfor a plain horizontal layout with no list semantics.
Props
The props the modes share. Type: ItemProps.
actions- Type:
ReactNode - Trailing content at the end of the row: buttons, a switch, a badge or a timestamp. With
href, controls here stay their own tab stops, above the link. as- Type:
"div" | "article" | "li" - Default:
"div" - The element to render.
ItemGroupalready wraps each Item in an<li>. className- Type:
string - Layout only: margins, grid placement (
mt-2). Appearance classes warn in development; usevariant. description- Type:
ReactNode - A short line under the title. Pass
<Text lines={2}>to clamp a long one. Withhref, it also describes the link (aria-describedby), so a screen reader that lands on the row hears its title and then this line: a result's byline and snippet, a recipe's time. media- Type:
ReactNode - Leading media: an
<img>(cropped square), anAvatar, or an icon withmediaSize="icon". Give an imagealt=""when the title already names it. mediaAbove- Type:
"sm" | "md" | "always" - Below this breakpoint the media leaves the row and runs full width above the text, at 4:3, as a recipe list shows its photos on a phone; above it, it sits beside the text at
mediaSize.alwayskeeps it above at every width. mediaSize- Type:
"sm" | "md" | "lg" | "icon" - Default:
"sm" - The media's size:
iconputs an icon in a muted 40px tile (and hides it from screen readers);smis 40px,md64px,lg112px for a recipe photo. ref- Type:
Ref<HTMLElement> - Reaches the rendered element.
size- Type:
"sm" | "md" - Default:
"md" - Row height:
mdhas 12px above and below,sm8px for dense lists. stackBelow- Type:
"sm" | "md" - Below this breakpoint the actions drop under the text, lined up with the title, as in a settings row on a phone. Without it they stay at the end at every width.
title(required)- Type:
ReactNode - What the row is: a name, a recipe title, a setting. It is the link's text with
href. variant- Type:
"outline" | "plain" - Default:
"plain" plainhas no frame, for rows in anItemGroup;outlineis a bordered, padded box for a row on its own.
Modes
ItemProps is a union: pass the props of exactly one mode. Mixing them is a type error.
With
Not in this mode (typed never):
external: Only withhref.href: Not withouthref. Addhrefto make the whole row a link.linkRef: Only withhref.
With href and external and linkRef
external- Type:
boolean - Opens the link in a new tab, safely, and tells screen readers so.
href(required)- Type:
string - Makes the whole row one link, with one tab stop, named by its title. Internal paths go through
OuroProvider'slinkComponent, as withLink. Controls anywhere in the row (a link in the description, a button in the media or the actions) stay their own tab stops and take their own clicks and taps. On a touch screen the row is at least 44px tall. linkRef- Type:
Ref<HTMLAnchorElement> - A ref to the link's
<a>, to move focus to it.
Native attributes
Also accepts the attributes in React's HTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.
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.
action: Not a prop. Ouro UI usesactions, for one control or several:<Item actions={<Button>…</Button>}>.asChild: Not a prop. Withhrefthe whole row is a link:<Item href="/recipes/1" title="…">.avatar: Not a prop. Ouro UI usesmedia:<Item media={<Avatar … />}>.icon: Not a prop. Ouro UI usesmedia:<Item media={<Icon />} mediaSize="icon">.image: Not a prop. Ouro UI usesmedia:<Item media={<img src="…" alt="" />}>.label: Not a prop. Ouro UI usestitle:<Item title="Braised short ribs">.subtitle: Not a prop. Ouro UI usesdescription:<Item description="Four hours">.thumbnail: Not a prop. Ouro UI usesmedia:<Item media={<img src="…" alt="" />}>.to: Not a prop. Ouro UI useshref, for router links too:<Item href="/recipes/1" title="…">.
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: Item 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/item/item.test.tsx::Item > passes attributes and a ref through, and lets layout classes win
verify:testExists(registry/ouro/components/ouro/item/item.test.tsx::Item > passes attributes and a ref through, and lets layout classes win)