Style Board

Fashion looks pinned on a masonry board of images in different shapes, with category filters and a heart to save the ones you like.

Pattern

≈ 1,350 tokens per use (tool 646 · guidance 310 · typical payload 394)How token costs are estimated

Example: Style Board

Show me spring outfit ideas I can save

Spring wardrobe ideas

Show

Tap the heart on the looks you like

When to use

  • The user wants to browse fashion looks or outfit ideas as a board, with images of different shapes, and maybe save the ones they like.
  • The agent has four to twelve real images of outfits, pieces or accessories, from a search or product tool.

When not to use

  • The agent doesn't have real image URLs. It must never invent or guess them.
  • There are only a few ideas, or the topic isn't clothes. Use Inspiration Cards.
  • The items are products to compare by price, size or rating. Use a Comparison Matrix or an Entity Card.
  • One scene-setting collage is enough. Use a Photo Mosaic.

How the agent should use it

  • Take every image src and url from a tool result or the host, never from memory. Only http(s) URLs and same-origin paths load; a link that isn't http(s) is dropped.
  • Pass each image's real aspect (width divided by height: 0.67 for a 2:3 portrait, 1 for a square). It reserves the pin's height before the image loads, so the board doesn't jump.
  • Keep titles to a few words. Add a price only for shoppable items, exactly as the store gives it.
  • Give each pin a one-word category when there are natural groups. Filter chips appear when there are two or more (the first four are shown, plus "All").
  • Set saveable: false when the user only wants to look. Otherwise they can heart looks and send them back.
  • Write real alt text for each image.
  • Lifecycle: looks are a snapshot, so it usually needs none. When they come from a live catalog with prices, set fetchedAt (and staleAfterMs). When the agent sends a new board, mark the old call superseded (supersededBy).

Interaction

  • Layout. Pins sit in two or three columns (columns, default 3; narrow containers always use 2). Each pin goes to the currently shortest column, so the columns end level and earlier pins never move when later ones stream in. Each pin's height comes from its aspect, so portraits, squares and landscapes mix like a pinboard. Screen readers hear the columns one after another.
  • Save. A heart on each pin's corner toggles it (aria-pressed, a 44px hit area). Saved pins get a ring, and the footer counts them ("3 looks saved"). Save to board sends the ids and locks the board; the footer then reads "Saved Linen trench and Cream blazer". With nothing saved the button is disabled and the instruction is read to screen readers only.
  • Filter. Category chips (they wrap onto more rows on narrow screens, never scroll sideways) show only the matching pins, rebalanced across the columns. Saves are kept when the filter changes.
  • Links. A pin with a safe url is a link that opens in a new tab and sends { action: "open", id } for analytics.
  • Broken images. An image that fails to load, or whose src isn't allowed, becomes a neutral tile showing its alt text.
  • Streaming. Pins append as they arrive, with a placeholder for the next one; saving is disabled until the call completes. Images load lazily.

Installation

npx shadcn@latest add https://your-site.com/r/style-board.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { styleBoardTool } from "@/components/genui/style-board.schema"

const tools = {
  [styleBoardTool.name]: tool({
    description: styleBoardTool.description,
    inputSchema: styleBoardTool.inputSchema,
  }),
}

With saveable: false the board is display-only, so give the tool an execute that returns immediately. Otherwise the agent receives:

{ action: "save", ids: ["trench", "floral"] } // in board order

Link opens send { action: "open", id: "trench" }. They are analytics only and never lock the board; the agent doesn't need to answer them.

Restoring from history

When a conversation is reloaded, pass the stored result back as result. A save result restores the saved looks (hearts, rings and the footer) read-only and never calls onResult again, so a choice can't be sent twice. With the AI SDK, that's the tool call's output:

<StyleBoard
  {...part.input}
  result={part.state === "output-available" ? part.output : undefined}
/>

Schema

PropTypeNotes
titlestring?e.g. "Spring wardrobe ideas"
columns2 | 3Default 3. Narrow containers always show 2
saveablebooleanDefault true. false is display-only
submitLabelstring?Save button text. Default "Save to board"
pinsPin[]4–12 pins, below

A pin is { id, image: { src, alt, aspect? }, title, price?, category?, url? }. image.src, image.aspect, price and url are data-bound; title is up to 60 characters, category up to 20.

Host props: status (streaming, ready, saved), onResult, result and className.

Source

On this page