Skip to content
Sign in
Image studioGeneration feedMenu

Generation feedPro

Every generation newest first, grouped by prompt: a masonry per prompt under a sticky header, virtualized so thousands of images scroll at full speed, with arrow keys through the masonry and per image actions on hover and focus.

ProPrompts newest first under sticky headers, one still generating and one failed tile. Tab or hover for actions, Enter opens the viewer.

Installation

$ pnpm dlx shadcn@latest add @chatcn-pro/generation-feed

Usage

tsx
import { GenerationFeed, GenerationFeedAction } from "@/components/ui/chatcn/pro/generation-feed"

<GenerationFeed
  groups={history}
  className="h-[640px]"
  onOpen={(image, group, tile) => openViewer(image, group, tile)}
  renderActions={() => (
    <GenerationFeedAction label="Download">
      <DownloadIcon />
    </GenerationFeedAction>
  )}
/>

Give each image its intrinsic width and height: tiles take their final shape before anything loads, so the feed never shifts. Leave src out while an image is generating and pass progress when your job reports it; set status: "error" with an error that says how to fix it, and onRetry adds a Retry button.

The feed scrolls itself, so give it a height. It virtualizes by prompt and, inside a prompt, by tile, so only what is near the viewport exists in the page. The group holding the focused image always stays mounted, so focus is never lost to the virtualizer. Pass activeId to follow a lightbox: the feed scrolls the image into view when it changes.

Examples

Thousands of images

Two thousand images in 450 prompts, with 150 more prompts loading at the end through hasMore, loadingMore and onLoadMore.

ProTwo thousand images in 450 prompts, virtualized, loading 150 more prompts at the end.

States

ProFirst load, an empty history and a narrow frame.

Props

PropTypeNotes
groups{ id, prompt, model?, createdAt?, images }[]Newest first. Each image is { id, src?, thumbnail?, alt, width, height, status?, progress?, error?, parameters? }
onOpen(image, group, element) => voidEnter, Space or click on a ready tile. element is the tile, for focus return
renderActions({ image, group, tabIndex }) => ReactNodePer image actions, shown on hover and focus. Use GenerationFeedAction
renderGroupActions(group) => ReactNodeAt the end of each sticky header
onRetry(image, group) => voidShows Retry on failed tiles
activeId, defaultActiveId, onActiveChangestringThe image in the tab order. Setting it scrolls that image into view
loadingbooleanFirst page: a placeholder layout
hasMore, loadingMore, onLoadMoreonLoadMore runs when the end comes within two prompts of the viewport
emptyReactNodeReplaces the empty state
minColumnWidth, maxColumns, gap, paddingnumberDefaults 200, 5, 8 and 16. The column count follows the width
labelstringAccessible name. Default "Generations"
now, localenumber, stringReference time and locale for "4 min ago"

GenerationFeedAction takes a required label (the tooltip and the accessible name), an optional shortcut for the tooltip and the usual button props. It joins the tab order only on the active tile.

Keyboard

KeyAction
TabInto the feed on the active image, then its actions, then out
← → ↑ ↓Move through the masonry: up and down stay in a column, left and right go to the nearest tile in the next column, then on to the next prompt. Mirrored in right to left
Page Up, Page DownThe first image of the previous or next prompt
Home, EndFirst or last image of this prompt; with Ctrl or ⌘, of the feed
Enter, SpaceOpen the image

Accessibility

  • A role="feed" of <article> groups, each named by its prompt heading, with aria-posinset and aria-setsize (-1 while more can load). It is aria-busy while a page loads.
  • One tile is in the tab order (roving focus), so the feed is one tab stop however long it is.
  • Tiles are buttons named by the image's alt. Generating tiles read "Generating, 72%"; failed ones read the error.
  • Failure uses the red token with an icon and text, never color alone. Actions stay visible on touch screens, where there is no hover.

Motion

  • Images fade in over 200 ms once decoded, on a gray tile already at their final shape. An image scrolled back into view is cached and shows at once, with no second fade.
  • Per image actions fade in over 150 ms on hover or focus.
  • Keyboard moves are instant and scroll without smoothing. The sticky header gains its hairline instantly.
  • Under reduced motion nothing moves; the fades are short opacity changes only.