CodeGym /Courses /ChatGPT Apps /Inline patterns: cards, lists, carousels, and CTAs

Inline patterns: cards, lists, carousels, and CTAs

ChatGPT Apps
Level 8 , Lesson 1
Available

1. What the inline mode is and why it’s the “default”

OpenAI’s official guidelines emphasize: inline display is the primary mode for ChatGPT Apps. An inline widget is rendered right in the chat feed above the model’s reply and includes a small UI block (a card, a list, a carousel) plus a follow-up message from GPT underneath it.

The idea is simple: instead of sending the user to a separate, large interface, we give them a compact visual “burst” right in the conversation context: the model explains what happened and what the next options are, while the widget neatly shows structure and available actions.

An inline widget:

  • is lightweight in content and doesn’t require many steps;
  • doesn’t drag the user into complex navigation with tabs and internal scrollbars;
  • solves one or two small tasks: show a list of options, let the user choose, confirm an action, show status.

The fullscreen mode (covered in the next lecture) is for large wizards and complex content. For now, the key point is: by default, think inline, and only opt into fullscreen when inline clearly can’t handle it.

Our goal in this lecture is to confidently operate the three main inline patterns and layer CTA on top of them:

  • cards
  • lists
  • carousels

as well as to correctly attach CTA buttons (Call to Action).

2. When inline is better than fullscreen

Put simply, the inline mode is a “quick helper,” while fullscreen is a “separate app inside ChatGPT.”

Inline is especially good when:

  • you need to show a few options and let the user pick one or two;
  • the result can be expressed in a compact structure: a gift card, an order summary, a mini table;
  • the user performs a short action: “choose,” “show details,” “change filter”;
  • the dialog remains primary: GPT explains, jokes, comments, while the widget simply provides a convenient form or visual.

In GiftGenius terms, inline means:

  • show 3–5 top gifts for the selected person;
  • offer a quick filter: “show digital gifts only”;
  • confirm the selection: “here’s the order summary — all good?”

You’ll use fullscreen later for a three-step wizard of a complex checkout. For now, we stay in the lightweight zone: one tool call result → one inline widget.

For clarity — a small table:

Pattern When it fits perfectly Example in GiftGenius
Card 1–3 entities with key parameters and a CTA Top 3 gifts
List 5–10 text items, readability matters a list of ideas without images
Carousel 3–8 similar options with visuals, scrolling needed a long list of gifts

With that in mind, let’s get concrete: how these patterns are implemented in UI and code. We’ll go through each pattern in the same format: first — what it is from a UX perspective, then — a simple React component for GiftGenius, and finally — how all this fits into an inline widget.

3. Cards: the basic brick of inline UI

What a card is in the context of the Apps SDK

According to OpenAI guidelines, an inline card is a lightweight, single-user widget that shows a small amount of structured data and 1–2 actions at the bottom. It can have a title, an image, a couple of metadata lines, and one primary CTA button (plus an optional secondary one).

In GiftGenius, each card is one gift. It’s convenient to include:

  • the gift’s title;
  • the price;
  • who it’s suitable for (e.g., “colleague,” “close friend”);
  • a short explanation of why it’s a good option;
  • a button like “Choose this gift” or “More details.”

A card should be self-contained: with a glance, the user understands what the entity is and what the primary action is.

Data type and a simple GiftCard component

First, let’s define the data type for a gift. Assume we already have a ToolOutput with an array of such objects; here we only care about the UI part.

// General gift structure for UI
export type GiftSuggestion = {
  id: string;
  title: string;
  priceLabel: string;         // e.g., "≈ $40"
  recipientLabel: string;     // "for a colleague"
  reason?: string;            // explanation from the model
  imageUrl?: string;
};

Now let’s build a simple React card component:

type GiftCardProps = {
  gift: GiftSuggestion;
  onSelect: (gift: GiftSuggestion) => void;
};

export function GiftCard({ gift, onSelect }: GiftCardProps) {
  return (
    <div className="flex flex-col gap-2 rounded-lg border p-3">
      <div className="text-sm font-medium">{gift.title}</div>
      <div className="text-xs text-muted-foreground">
        For: {gift.recipientLabel} · {gift.priceLabel}
      </div>
      {gift.reason && (
        <div className="text-xs text-muted-foreground">{gift.reason}</div>
      )}
      <button
        className="mt-2 self-start rounded bg-primary px-3 py-1 text-xs text-primary-foreground"
        onClick={() => onSelect(gift)}
      >
        Select this gift
      </button>
    </div>
  );
}

A few nuances up front:

  • don’t overload the card with text — at most 2–3 metadata lines and a short explanation;
  • one primary CTA — “Select this gift”; don’t try to cram in five different options;
  • the component is easy to reuse in an inline list and inside a carousel.

How cards fit into the overall widget

Assume we have an array of gifts returned after calling giftgenius.suggestGifts via our MCP. An inline widget can simply render them in a 1–3 column grid.

type GiftGridProps = {
  gifts: GiftSuggestion[];
  onSelect: (gift: GiftSuggestion) => void;
};

export function GiftGrid({ gifts, onSelect }: GiftGridProps) {
  return (
    <div className="grid gap-3 sm:grid-cols-2">
      {gifts.map((gift) => (
        <GiftCard key={gift.id} gift={gift} onSelect={onSelect} />
      ))}
    </div>
  );
}

Here we:

  • use a 1–2 column grid so the widget doesn’t turn into a “brick wall”;
  • can easily limit the number of cards, e.g., show only the first 3–6.

The onSelect handler can call a checkout tool or simply save the selection to Widget State and let the model continue the dialog. A minimal example of integrating with a tool:

async function handleSelect(gift: GiftSuggestion) {
  await window.openai.actions.call("giftgenius.startCheckout", {
    giftId: gift.id,
  });
}

Here, window.openai.actions.call is the bridge to invoke a registered MCP tool right from the widget.

Typically, after such a call the model will show either a status or open the next widget (e.g., an order summary). The main thing is — don’t try to implement the entire checkout logic inside the card; the card should trigger a clear next step.

4. Lists: when visuals aren’t the main thing

If a card is a small “poster,” a list is a neat textual enumeration. Documentation and UX recommendations show that lists are good when the content of the text matters more than a bright visual accent.

A list is a good fit when:

  • you need to show 5–10 options, but they don’t require images;
  • the user wants to quickly scan names and short descriptions;
  • all items share the same action, and the UI shouldn’t distract.

Examples in GiftGenius:

  • a list of “quick” gift ideas without details;
  • a list of favorite categories: “for colleagues,” “for parents,” “for kids”;
  • a list of saved collections (“Gifts for the HR department,” “Holiday stocking stuffers under $20”).

A simple list component

Let’s make a compact list with a single CTA button “More details” on the right.

type GiftListProps = {
  gifts: GiftSuggestion[];
  onSelect: (gift: GiftSuggestion) => void;
};

export function GiftList({ gifts, onSelect }: GiftListProps) {
  return (
    <ul className="flex flex-col gap-2">
      {gifts.map((gift) => (
        <li
          key={gift.id}
          className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"
        >
          <span className="truncate">{gift.title}</span>
          <button
            className="text-xs text-primary"
            onClick={() => onSelect(gift)}
          >
            More details
          </button>
        </li>
      ))}
    </ul>
  );
}

Here we:

  • add truncate to the title so long names don’t break the layout;
  • again use a single CTA per item;
  • keep all the “rich” information (description, image, reviews) for the next step — e.g., on click, open a separate card or a fullscreen view.

A list works especially well with follow-up suggestions from GPT. The widget shows a list of “candidates,” and under it GPT writes something like:

“I can narrow to gifts under $30 or show only digital ones. What should we pick?” and offers two or three follow-up buttons.

In a separate section, we’ll also cover how best to combine inline widgets and follow-up messages in different scenarios.

5. Carousels: when there are many options, but they’re all similar

A carousel is a set of cards arranged horizontally and navigated by swipe or arrow buttons. Guidelines recommend using carousels when you show a small list of similar items (usually 3–8), each containing an image, a title, and a bit of metadata.

The main idea: the user can quickly scan a set of options without getting buried under an endless vertical list.

In GiftGenius, a carousel is helpful if:

  • we have 10–15 suitable gifts, but the inline widget should show only the “hot eight”;
  • each gift is visually appealing (image, styling);
  • it’s important that the user flips through options without scrolling far down the chat.

UX rules for carousels

Based on guidelines and research:

  • the number of cards in a carousel is from 3 to 8; if there are more, it’s better to provide a separate “Show more” command;
  • each card:
    • should have an image or another visual element;
    • shouldn’t contain more than two lines of metadata text;
    • has one clear CTA, such as “Select” or “More details”;
  • no complex nested navigation (tabs, sub-flows) inside the card;
  • avoid internal (vertical) scrollbars: let the card height adapt within a reasonable limit, but without its own scrollbar.

A simple “one card at a time” carousel

To avoid dealing with complex horizontal scrolling, you can implement the simplest option: show one card at a time and provide “previous/next” buttons.

import { useState } from "react";

type GiftCarouselProps = {
  gifts: GiftSuggestion[];
  onSelect: (gift: GiftSuggestion) => void;
};

export function GiftCarousel({ gifts, onSelect }: GiftCarouselProps) {
  const [index, setIndex] = useState(0);
  const gift = gifts[index];

  return (
    <div className="flex flex-col gap-2">
      <GiftCard gift={gift} onSelect={onSelect} />
      <div className="flex items-center justify-between text-xs">
        <button
          disabled={index === 0}
          onClick={() => setIndex((i) => i - 1)}
        >
          ← Previous
        </button>
        <span>
          {index + 1} / {gifts.length}
        </span>
        <button
          disabled={index === gifts.length - 1}
          onClick={() => setIndex((i) => i + 1)}
        >
          Next →
        </button>
      </div>
    </div>
  );
}

This already feels like a “carousel,” and meanwhile:

  • the code stays compact;
  • you don’t need to fight container width and horizontal scroll inside the widget;
  • it’s easy to limit gifts to 8 items before passing them to the component.

If you want a more “real” carousel, you can use overflow-x-auto and fixed card widths, but that’s a case where it’s simpler to take a ready-made component from a UI library (shadcn/ui, Radix-compatible solutions, etc.) rather than invent your own from scratch.

6. CTA buttons: few, clear, purposeful

CTA (Call to Action) is the heart of any inline pattern. Buttons are what turn your widget from a picture into a working tool.

Core principles

OpenAI documentation gives fairly strict recommendations:

  • on a card — at most two primary buttons (one main, one secondary);
  • in a carousel — ideally one CTA per item;
  • the CTA text should be a concrete verb: “Show details,” “Add to list,” “Proceed to payment,” not an abstract “OK” or “Action.”

The fewer buttons, the simpler it is for both the model and the user. Remember that there’s also a text part above and below the widget, plus GPT follow-up suggestions.

Binding CTA to application logic

In our GiftGenius, most CTAs will either:

  • change filters/selection criteria (a new tool call to giftgenius.refineSearch),
  • start checkout (giftgenius.startCheckout),
  • open an external site (via openExternal, which you already know from earlier lectures).

Example of a simple handler for the CTA “Change filters”:

async function handleRefineFilters(gift: GiftSuggestion) {
  await window.openai.actions.call("giftgenius.refineSearch", {
    baseGiftId: gift.id,
  });
}

From a UX standpoint, it’s very important to spell out in the system instructions when and exactly which CTA buttons the model should suggest. For example:

  • if the user asks “show more options,” it’s better to show a new carousel with a “Select” button;
  • if it’s time to purchase, the CTA “Proceed to payment” should lead to a tool call that starts an ACP checkout (we’ll get to this in the module on commerce and payments).

Another helpful practice is not to duplicate ChatGPT functions in your CTA. Don’t make a “Ask ChatGPT” button — the user already has the input field and voice. The guidelines explicitly recommend avoiding “duplicated” inputs inside the card.

7. Inline + follow-up: a duet

An inline widget never lives in a vacuum. A typical reply structure looks like this:

  1. the model decides to use your App and call a tool;
  2. your MCP returns data;
  3. ChatGPT renders an inline widget with this data;
  4. under it, the model adds a short follow-up and ready-made paths forward.

For GiftGenius, it might look like this:

  • an inline widget: three gift cards with a CTA “Select”;
  • text below:
    “Here are three ideas for a colleague: a desk lamp, a public speaking course, and a coffee shop gift card. I can: — show only options under $30; — find a few more ideas in a similar style; — help you proceed to purchase one of them.””

In the follow-up, the model can reference your widget’s CTA (“click ‘Select’ under the option you like”) or suggest textual commands, which will again result in a tool call and a re-render of the inline UI.

It’s important to remember: the widget doesn’t have to do everything. Sometimes it’s better to leave part of the scenario to the text dialog and use the widget as a “visual block” inside the conversation.

8. How this fits into GiftGenius’s overall flow

To make it clearer, let’s summarize with a simple sequence diagram:

sequenceDiagram
  participant U as User
  participant C as ChatGPT
  participant A as GiftGenius Widget
  participant B as MCP/Backend

  U->>C: "Find 3 gifts up to $50 for a colleague"
  C->>B: call_tool(giftgenius.suggestGifts)
  B-->>C: Top 3 options
  C->>A: render inline widget (cards/carousel)
  A-->>U: cards with CTA “Select”
  U->>A: click on CTA
  A->>B: call_tool(giftgenius.startCheckout)
  B-->>A: status / payment link
  A-->>U: selection summary / status
  C-->>U: follow-up: "I can find more ideas or help with a greeting card"

From an architectural point of view:

  • MCP stays the “brains” (selection, business logic, ACP),
  • the widget is the “face” (cards/lists/carousels),
  • ChatGPT is the “conversation lead,” explaining what happened and suggesting next steps.

To make this flow convenient:

  • don’t overload the widget with actions;
  • keep card data compact;
  • think through which follow-up options will be useful after each inline display.

9. A bit about the visual side of inline patterns

We’ll talk about visual design in detail in another lecture of the module, but a few points critical to inline patterns are worth mentioning now.

First, make sure your cards and lists don’t look like a foreign site inside ChatGPT. Colors and spacing should be tidy, without neon gradients and Comic Sans fonts. An inline widget is part of the overall ChatGPT UI, not a 2007 banner.

Second, avoid internal scrollbars. If your card is so long that it has its own scrollbar, something went wrong: either you’re trying to cram in too much content, or the pattern is wrong (maybe you need fullscreen).

Third, keep density in check:

  • there should be visible spacing between cards;
  • CTA should be easy to tap (adequate padding);
  • text should be readable even on mobile, without microscopic fonts.

This may sound like “design nitpicking,” but in practice: if an inline widget looks “native,” the model uses it more readily, and users get less confused.

10. Practice: how to evolve GiftGenius based on this lecture

If you want to reinforce the material, here’s a simple practical checklist:

First, take the current result of the giftgenius.suggestGifts tool (an array of gifts) and:

  1. Implement three different UI variants in one component:
    • GiftGrid with cards;
    • GiftList with a textual list;
    • GiftCarousel with “previous/next” navigation.
  2. Add one or two CTA buttons to each:
    • for cards — “Select”;
    • for lists — “More details”;
    • for carousels — also “Select,” plus a separate “Show more options” button under the widget.
  3. Depending on the state (e.g., how many gifts the tool returned), choose which pattern to use:
    • if there are few options (≤ 3) — a card grid;
    • if there are many text ideas — a list;
    • if there are many visual gifts — a carousel.

This way you’ll not only practice the UI but also start thinking about dynamic pattern selection depending on context — which both users and Store reviewers will love.

In general, inline patterns are a quick, lightweight UI layer that lives right in the chat feed and doesn’t try to replace a standalone app. Cards, lists, and carousels cover 80% of typical tasks: show options, let the user choose, and smoothly continue the dialog.

In the next lecture of this module we’ll look at what to do when inline no longer “keeps up”: we’ll go through fullscreen wizards, PiP mode, and scenarios where your App truly needs a dedicated large screen inside ChatGPT.

11. Common mistakes when working with inline patterns

Mistake #1: turning the inline widget into a mini-site.
Sometimes developers try to cram tabs, accordions, forms, a table, and many other elements into a single card. The result is a heavy UI that breaks the chat rhythm and becomes inconvenient on mobile devices. The guidelines explicitly say: no deep navigation and complex views inside inline cards; complex scenarios go to fullscreen.

Mistake #2: too many CTA buttons.
“How about we put ‘More details,’ ‘Buy,’ ‘Favorite,’ ‘Share,’ ‘Report,’ and ‘Generate a greeting card’ on the card?” The user gets lost, the model too, and the chance of clicking the right button drops. Remember the rule: one primary CTA and at most one secondary. Other scenarios are better moved to a GPT follow-up or subsequent steps.

Mistake #3: mixing list, cards, and carousel in one answer without a reason.
If the same content is shown as a list, then cards, then a carousel “just because we can,” the user loses a sense of consistency. It’s better to pick one pattern for a specific type of output (e.g., ideas without images — list; gifts with images — carousel) and stick to it.

Mistake #4: overloading cards with text.
A card with three paragraphs of description, three prices, and two “why this is great” blocks turns into a wall of text. The user stops scanning it and just scrolls past. Try to keep only the essentials on the card: title, one key parameter, one short reason, and a CTA. Everything else can be explained in the GPT text response nearby.

Mistake #5: relying only on UI and ignoring the follow-up dialog.
Sometimes you’ll see the approach “we do everything via buttons; the user doesn’t need to talk.” That contradicts the very idea of ChatGPT. An inline widget should complement the dialog, not replace it. Don’t forget to think through which follow-up options the model can suggest under the widget: change filters, request more options, proceed to the next step.

Mistake #6: ignoring limits on the number of items.
A carousel of 25 cards or a list of 50 items inside one inline widget is a sure way for the user to scroll past everything. The documentation recommends 3–8 items in a carousel and 5–10 items in a list. If there’s more data, it’s helpful to add a CTA like “Show more” or “Show everything as text.”

Mistake #7: using inline where fullscreen is already needed.
There’s a temptation to “do everything inline,” even when you already have 4 steps, forms with a dozen fields, and big tables. You’ll either end up with a monster or start inventing nested scrolls and pseudo-steppers inside cards. As soon as you feel that the number of steps and fields is growing — that’s a signal to consider switching to a fullscreen wizard and keeping inline for quick previews and action summaries.

1
Task
ChatGPT Apps, level 8, lesson 1
Locked
Inline-style mini card + a single CTA (openExternal)
Inline-style mini card + a single CTA (openExternal)
1
Task
ChatGPT Apps, level 8, lesson 1
Locked
Inline list (5–10 rows) + CTA "Learn more" → follow‑up to chat
Inline list (5–10 rows) + CTA "Learn more" → follow‑up to chat
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION