Item Carousel

Horizontal carousel for browsing collections.

Deadwood

HBO · 2004

The Wire

HBO · 2002

Twin Peaks

ABC · 1990

The Simpsons

Fox · 1989

Mad Men

AMC · 2007

Peep Show

Channel 4 · 2003

The Sopranos

HBO · 1999

<ItemCarousel  id="item-carousel-recommendations"  items={[      {          "id": "rec-1",          "name": "Deadwood",          "subtitle": "HBO · 2004",          "color": "#8b6f47",          "actions": [              {                  "id": "info",                  "label": "Details",                  "variant": "secondary"              },              {                  "id": "watch",                  "label": "Watch"              }          ]      },      {          "id": "rec-2",          "name": "The Wire",          "subtitle": "HBO · 2002",          "color": "#1e293b",          "actions": [              {                  "id": "info",                  "label": "Details",                  "variant": "secondary"              },              {                  "id": "watch",                  "label": "Watch"              }          ]      },      {          "id": "rec-3",          "name": "Twin Peaks",          "subtitle": "ABC · 1990",          "color": "#7f1d1d",          "actions": [              {                  "id": "info",                  "label": "Details",                  "variant": "secondary"              },              {                  "id": "watch",                  "label": "Watch"              }          ]      },      {          "id": "rec-4",          "name": "The Simpsons",          "subtitle": "Fox · 1989",          "color": "#fbbf24",          "actions": [              {                  "id": "add",                  "label": "Add to List"              }          ]      },      {          "id": "rec-5",          "name": "Mad Men",          "subtitle": "AMC · 2007",          "color": "#c2410c",          "actions": [              {                  "id": "info",                  "label": "Details",                  "variant": "secondary"              },              {                  "id": "watch",                  "label": "Watch"              }          ]      },      {          "id": "rec-6",          "name": "Peep Show",          "subtitle": "Channel 4 · 2003",          "color": "#1e40af",          "actions": [              {                  "id": "info",                  "label": "Details",                  "variant": "secondary"              },              {                  "id": "watch",                  "label": "Watch"              }          ]      },      {          "id": "rec-7",          "name": "The Sopranos",          "subtitle": "HBO · 1999",          "color": "#991b1b",          "actions": [              {                  "id": "info",                  "label": "Details",                  "variant": "secondary"              },              {                  "id": "watch",                  "label": "Watch"              }          ]      }  ]}  onItemClick={(itemId) => console.log("Clicked:", itemId)}  onItemAction={(itemId, actionId) => console.log("Action:", itemId, actionId)}/>

A vertical list of ten search results buries everything below the fold. ItemCarousel lays them out in a scrollable row of cards so the user can browse products, restaurants, or any collection the assistant returns without losing their place in the conversation. Each card shows an image, subtitle, and action buttons.

Role: Information. For displaying data without user input. See Design Guidelines for how component roles work.

Getting Started

Run this once from your project root.

npx shadcn@latest add @tool-ui/item-carousel

Render ItemCarousel in your UI with tool-compatible props.

import { ItemCarousel } from "@/components/tool-ui/item-carousel";export function Example({ payload }: { payload: React.ComponentProps<typeof ItemCarousel> }) {  return <ItemCarousel {...payload} />;}

Registration tells assistant-ui which component to render when a tool named searchItems returns data. Without it, tool results appear as raw JSON.

// Backend toolimport { tool } from "ai";import { z } from "zod";import { SerializableItemCarouselSchema } from "@/components/tool-ui/item-carousel/schema";const searchItems = tool({  description: "Search for items matching criteria",  inputSchema: z.object({    query: z.string(),    maxPrice: z.number().optional(),  }),  outputSchema: SerializableItemCarouselSchema,  async execute({ query, maxPrice }) {    const items = await fetchItems(query, maxPrice);    return {      id: `item-search-${query}`,      items: items.map((item) => ({        id: item.id,        name: item.name,        subtitle: `$${item.price.toFixed(2)}`,        image: item.imageUrl,        actions: [          { id: "view", label: "View Details" },          { id: "select", label: "Select", variant: "default" },        ],      })),    };  },});// Frontend with assistant-uiimport { type Toolkit } from "@assistant-ui/react";import { ItemCarousel } from "@/components/tool-ui/item-carousel";import { safeParseSerializableItemCarousel } from "@/components/tool-ui/item-carousel/schema";import { createResultToolRenderer } from "@/components/tool-ui/shared";export const toolkit: Toolkit = {  searchItems: {    type: "backend",    render: createResultToolRenderer({      safeParse: safeParseSerializableItemCarousel,      render: (parsedResult) => (        <>          <ItemCarousel            {...parsedResult}            onItemClick={(id) => router.push(`/items/${id}`)}            onItemAction={(itemId, actionId) => {              if (actionId === "select") {                selectItem(itemId);              }            }}          />        </>      ),    }),  },};

Key Features

Collection browsing

Equal-height cards via CSS Grid so items line up regardless of title length

Smart scrolling

Smooth scroll with snap points and reduced-motion fallback

Per-card actions

Buttons stack or sit side-by-side based on card width via container queries

Full accessibility

Enter/Space activation, focus rings, and ARIA roles throughout

Design Details

How the carousel handles common interaction patterns:

Responsive button layout: Each card is a container (@container/card). Narrow cards stack buttons vertically with the primary on top. Above 176px, buttons flow horizontally with the primary on the right. flex-col-reverse keeps DOM order consistent while adapting visual hierarchy.

Equal card heights: Cards use CSS Grid (grid-flow-col with auto-cols-max). Grid cells match heights automatically, so a two-line title aligns with one-line neighbors.

Smooth scroll animation: Custom JavaScript scrolling with a cubic-bezier ease-out curve. Respects prefers-reduced-motion with an instant-scroll fallback. Snap positions are calculated dynamically from card offsets.

Image handling: Accepts image URLs or hex color fallbacks. Images use loading="lazy" and decoding="async". On hover, images scale up 5% with a duration-200 transform transition.

Overlay click target: Interactive cards render a full-card overlay button (absolute inset-0, z-10) for onItemClick, while action buttons sit above it (z-20) so primary actions stay independently clickable.

Touch optimization: touch-manipulation eliminates the 300ms tap delay on iOS. Navigation buttons appear only on hover/focus (desktop) with backdrop blur.

Props

Prop

Type

Item Schema

Prop

Type

Accessibility

  • Scroll animations respect prefers-reduced-motion and fall back to instant scrolling
  • Cards are keyboard-navigable with Enter/Space activation
  • Proper ARIA roles and labels for screen reader announcements
  • Focus rings visible for keyboard navigation
  • Data Table: both present collections, but DataTable is better for dense, sortable comparisons
  • Image Gallery: for image-heavy collections where the visuals are the primary content