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.

Four hours, mostly hands-off
Sync token
recipes:read · Created May 2

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-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/item#v1.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/item

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/item#v1.2.13 ourostack/ouro-ui/item-tests#v1.2.13
import { 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 Card instead for a larger tile with a cover image and a body, and Row for 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. ItemGroup already wraps each Item in an <li>.
className
Type: string
Layout only: margins, grid placement (mt-2). Appearance classes warn in development; use variant.
description
Type: ReactNode
A short line under the title. Pass <Text lines={2}> to clamp a long one. With href, 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), an Avatar, or an icon with mediaSize="icon". Give an image alt="" 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. always keeps it above at every width.
mediaSize
Type: "sm" | "md" | "lg" | "icon"
Default: "sm"
The media's size: icon puts an icon in a muted 40px tile (and hides it from screen readers); sm is 40px, md 64px, lg 112px for a recipe photo.
ref
Type: Ref<HTMLElement>
Reaches the rendered element.
size
Type: "sm" | "md"
Default: "md"
Row height: md has 12px above and below, sm 8px 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"
plain has no frame, for rows in an ItemGroup; outline is 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 with href.
  • href: Not without href. Add href to make the whole row a link.
  • linkRef: Only with href.

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's linkComponent, as with Link. 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 uses actions, for one control or several: <Item actions={<Button>…</Button>}>.
  • asChild: Not a prop. With href the whole row is a link: <Item href="/recipes/1" title="…">.
  • avatar: Not a prop. Ouro UI uses media: <Item media={<Avatar … />}>.
  • icon: Not a prop. Ouro UI uses media: <Item media={<Icon />} mediaSize="icon">.
  • image: Not a prop. Ouro UI uses media: <Item media={<img src="…" alt="" />}>.
  • label: Not a prop. Ouro UI uses title: <Item title="Braised short ribs">.
  • subtitle: Not a prop. Ouro UI uses description: <Item description="Four hours">.
  • thumbnail: Not a prop. Ouro UI uses media: <Item media={<img src="…" alt="" />}>.
  • to: Not a prop. Ouro UI uses href, 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