Display multiple items side-by-side for easy comparison with equal-height cards via CSS Grid
Custom scroll animation with cubic-bezier easing, snap points, and reduced motion support
Buttons adapt via container queries: stacked on narrow cards, side-by-side when wider
Keyboard navigation (Enter/Space), focus rings, ARIA roles, and screen reader labels
How the Item Carousel handles common interaction patterns:
Responsive button layout: Each card is its own container (@container/card). When the card is narrow, buttons stack vertically with the primary action on top. Above 176px, they flow horizontally with primary on the right. This uses flex-col-reverse to maintain consistent DOM order while adapting visual hierarchy.
Equal card heights: Cards use CSS Grid (grid-flow-col with auto-cols-max) rather than flexbox. Grid cells in the same row automatically match heights, so a card with a two-line title aligns perfectly with one-line neighbors.
Smooth scroll animation: The carousel implements custom JavaScript scrolling with a cubic-bezier ease-out curve. It respects prefers-reduced-motion and falls back to instant scrolling. Navigation calculates snap positions dynamically based on actual card offsets.
Image handling: Supports both image URLs and hex color fallbacks. Images use loading="lazy" and decoding="async" for performance. On hover, images scale up 5% with a 300ms transition.
Click delegation: Cards can be interactive (onItemClick) while containing action buttons. Click handlers check if the event originated from a button and delegate appropriately, preventing double-firing.
Touch optimization: Cards use touch-manipulation to eliminate the 300ms tap delay on iOS. Navigation buttons appear only on hover/focus (desktop) and use backdrop blur for visual polish.
Copy components/tool-ui/item-carousel and the shared directory into your project. The shared folder contains utilities used by all Tool UI components.
This component requires the following shadcn/ui components:
pnpm dlx shadcn@latest add button card// Backend toolimport { tool } from "ai";import { z } from "zod";import { SerializableItemCarouselSchema } from "@/components/tool-ui/item-carousel";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 { makeAssistantToolUI } from "@assistant-ui/react";import { ItemCarousel } from "@/components/tool-ui/item-carousel";export const SearchItemsUI = makeAssistantToolUI({ toolName: "searchItems", render: ({ result }) => ( <ItemCarousel {...result} onItemClick={(id) => router.push(`/items/${id}`)} onItemAction={(itemId, actionId) => { if (actionId === "select") { selectItem(itemId); } }} /> ),});Prop
Type
Prop
Type
prefers-reduced-motion and fall back to instant scrollingHBO · 2004
HBO · 2002
ABC · 1990
Fox · 1989
AMC · 2007
Channel 4 · 2003
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)}/>pnpm dlx shadcn@latest add button card// Backend tool
import { tool } from "ai";
import { z } from "zod";
import { SerializableItemCarouselSchema } from "@/components/tool-ui/item-carousel";
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-ui
import { makeAssistantToolUI } from "@assistant-ui/react";
import { ItemCarousel } from "@/components/tool-ui/item-carousel";
export const SearchItemsUI = makeAssistantToolUI({
toolName: "searchItems",
render: ({ result }) => (
<ItemCarousel
{...result}
onItemClick={(id) => router.push(`/items/${id}`)}
onItemAction={(itemId, actionId) => {
if (actionId === "select") {
selectItem(itemId);
}
}}
/>
),
});<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)}
/>