Inline Card Optimization for ChatGPT Apps: Best Practices
Over 60% of ChatGPT app submissions are rejected for inline card violations—specifically exceeding the 2-CTA limit or implementing nested scrolling. These constraints aren't arbitrary; they're designed to maintain ChatGPT's conversational rhythm and prevent overwhelming users. Understanding how to optimize inline cards for ChatGPT apps is critical for passing OpenAI review on first submission.
This optimization guide is part of our ChatGPT App Analytics & Optimization series, where you'll learn how to measure and improve inline card performance.
In this guide, you'll learn:
- Why inline cards have a maximum of 2 primary CTAs
- How to structure content hierarchy without nested scrolling
- Optimized action design patterns that pass OpenAI review
- Performance benchmarks for inline card load times
- Real-world before/after examples from approved apps
Inline Card Constraints
ChatGPT's inline card display mode has strict constraints designed to maintain chat rhythm and prevent overwhelming users. Understanding these constraints is critical for chatgpt inline cards optimization and avoiding automatic rejection during OpenAI review.
Maximum 2 Primary Actions Per Card
Critical Constraint: Each inline card can display a maximum of 2 primary call-to-action (CTA) buttons.
Wrong Approach (Will Be Rejected):
// ❌ Too many CTAs - will cause rejection
const card = {
content: "Choose your workout plan",
actions: [
{ label: "View Details", type: "primary" },
{ label: "Compare Plans", type: "primary" },
{ label: "Start Trial", type: "primary" },
{ label: "Contact Sales", type: "secondary" }
]
}
Correct Approach:
// ✅ Maximum 2 primary CTAs
const card = {
content: "Choose your workout plan",
actions: [
{ label: "Start Trial", type: "primary" },
{ label: "View Details", type: "secondary" }
]
}
Why This Matters: OpenAI found that more than 2 CTAs causes decision paralysis and breaks chat flow. Apps violating this constraint are automatically rejected during review.
No Nested Scrolling
Inline cards MUST NOT contain internal scrolling regions. All content must be visible without scrolling within the card.
Anti-Pattern:
// ❌ Nested scrolling - will cause rejection
<div style="max-height: 300px; overflow-y: scroll;">
<ul>
{workouts.map(w => <li>{w.name}</li>)}
</ul>
</div>
Best Practice:
// ✅ Use carousel for multiple items
const carousel = {
type: "carousel",
items: workouts.slice(0, 8), // Max 8 items per carousel
itemTemplate: (workout) => ({
title: workout.name,
metadata: [workout.duration, workout.difficulty],
action: { label: "Start Workout" }
})
}
Performance Note: Carousels automatically handle overflow and provide better mobile UX than scrollable lists. Keep to 3–8 items per carousel for scannability.
Content Size Limits
While OpenAI doesn't specify exact character limits, best practices suggest:
| Element | Recommended Max | Rationale |
|---|---|---|
| Title | 60 characters | Prevents truncation on mobile |
| Metadata | 120 characters | Readable at a glance |
| Description | 300 characters | Maintains chat rhythm |
Cards exceeding these limits risk poor mobile rendering and lower engagement.
Content Hierarchy
The visual structure of your inline card determines whether users understand their options at a glance or abandon the interaction. Effective chatgpt inline cards optimization requires prioritizing information from most to least important.
Title-First Design
Your card title is the anchor point for user attention. It should answer "What am I looking at?" in 3-5 words.
// ✅ Clear, action-oriented title
const fitnessCard = {
title: "High-Intensity Interval Training",
metadata: ["30 min", "Intermediate", "Cardio Focus"],
description: "Build endurance with alternating sprint and recovery intervals."
}
Design Hierarchy:
- Title (60 chars max) - Primary information
- Metadata (2-3 lines) - Supporting details
- Description (300 chars max) - Context
- Actions (1-2 CTAs) - Next steps
Metadata Optimization
Metadata provides context without cluttering the card. Reduce metadata to the most relevant details, with three lines max.
Wrong Approach:
// ❌ Too much metadata - overwhelms users
metadata: [
"Duration: 30 minutes",
"Difficulty: Intermediate",
"Equipment: Dumbbells, Resistance Bands",
"Calories Burned: 300-400",
"Trainer: Sarah Johnson",
"Category: Cardio"
]
Optimized Approach:
// ✅ Essential metadata only
metadata: [
"30 min • Intermediate",
"Dumbbells required"
]
Why This Works: Users scan cards in under 2 seconds. Only the most decision-critical information should appear in metadata.
Badge Usage
Use badges to highlight supporting context like discounts, new features, or status indicators.
const restaurantCard = {
title: "Luigi's Italian Bistro",
metadata: ["4.8 stars", "Italian • $$"],
badge: "20% Off First Order",
action: { label: "View Menu" }
}
Best Practices:
- Maximum 1 badge per card
- Use for time-sensitive or high-value information
- Keep badge text under 20 characters
Action Design
Your CTA buttons are the gateway to user engagement. Poorly designed actions lead to low click-through rates and abandoned workflows.
Action-Oriented Language
CTA labels should clearly describe the outcome, not the interaction.
Before: Poor CTA Design
// ❌ Vague, uninformative CTAs
<Button onClick={handleClick}>Click Here</Button>
<Button onClick={handleSubmit}>Submit</Button>
Problems:
- No context about what happens after clicking
- Generic labels don't guide user decision
- Duplicates ChatGPT's native functionality (user can just type "submit")
After: Optimized CTA Design
// ✅ Clear, action-oriented CTAs
<Button
onClick={() => scheduleWorkout(workout.id)}
ariaLabel="Schedule 30-minute HIIT workout for tomorrow 6am"
>
Schedule for Tomorrow 6am
</Button>
<Button
onClick={() => viewWorkoutDetails(workout.id)}
variant="secondary"
ariaLabel="View detailed workout plan and equipment requirements"
>
View Workout Details
</Button>
Improvements:
- Specific action outcome ("Schedule for Tomorrow 6am" vs "Click Here")
- Includes key details (time, workout type)
- Accessible (aria-label for screen readers)
- Unique value (can't achieve this scheduling specificity with plain ChatGPT)
- Clear hierarchy (primary vs secondary button)
Approval Tip: OpenAI reviewers specifically look for CTAs that provide value beyond ChatGPT's base capabilities. Generic "Submit" buttons are a red flag.
Primary vs Secondary Actions
When you need 2 CTAs, establish clear hierarchy:
Primary Action:
- Most common user goal
- Highest visual prominence
- Advances the workflow
Secondary Action:
- Alternative or exploratory option
- Lower visual weight
- Provides context or comparison
// ✅ Clear action hierarchy
actions: [
{
label: "Start 7-Day Free Trial",
type: "primary",
onClick: () => window.openai.callTool("start_trial")
},
{
label: "Compare Plans",
type: "secondary",
onClick: () => window.openai.requestDisplayMode("fullscreen")
}
]
Avoid Deep Navigation
Inline cards should not contain multiple drill-ins, tabs, or deeper navigation. If your workflow requires nested views, use fullscreen display mode instead.
Anti-Pattern:
// ❌ Multi-level navigation in inline card
<Card>
<Tabs>
<Tab>Overview</Tab>
<Tab>Reviews</Tab>
<Tab>Location</Tab>
</Tabs>
<TabPanel>
<!-- Deep content here -->
</TabPanel>
</Card>
Best Practice:
// ✅ Single action that transitions to fullscreen
<Card>
<Title>Luigi's Italian Bistro</Title>
<Metadata>4.8 stars • Italian • $$</Metadata>
<Button onClick={() => window.openai.requestDisplayMode("fullscreen")}>
View Full Details
</Button>
</Card>
For complex tasks that require multiple views, see our Display Modes Comparison Guide for guidance on when to use fullscreen instead of inline cards.
Performance
Inline cards should render in under 200ms to maintain chat rhythm. Slow-loading widgets disrupt conversation flow and increase abandonment rates.
Keep structuredContent Concise
The structuredContent payload is read by both the widget AND the ChatGPT model. Keep it well under 4k tokens for performance.
// ❌ Oversized structuredContent (exceeds 4k tokens)
return {
structuredContent: {
workouts: allWorkouts, // 500+ workout objects
reviews: allReviews, // 1000+ review objects
trainers: allTrainers // Full trainer database
}
}
// ✅ Concise structuredContent (under 1k tokens)
return {
structuredContent: {
workouts: topWorkouts.slice(0, 8), // Top 8 workouts only
summary: { totalWorkouts: 500, avgRating: 4.7 }
},
_meta: {
allWorkouts: allWorkouts // Large data in _meta (widget-only)
}
}
Performance Rule:
structuredContentis for the model to narrate._metais for the widget to render. Only put what the model needs instructuredContent.
Lazy Load Images
Images should use lazy loading to avoid blocking initial render.
// ✅ Lazy loading with placeholder
<img
src={workout.thumbnail}
loading="lazy"
alt={workout.name}
style={{ width: '100%', height: 'auto' }}
/>
Optimize Asset Size
| Asset Type | Max Size | Format |
|---|---|---|
| Thumbnails | 50 KB | WebP |
| Icons | 10 KB | SVG |
| Background | 100 KB | WebP/JPEG |
Examples
Example 1: Fitness Studio Booking Card
// Production-ready fitness studio inline card
const fitnessCard = {
title: "HIIT Cardio Blast",
metadata: ["30 min", "Intermediate", "Dumbbells required"],
badge: "New Class",
image: "/images/hiit-cardio.webp",
actions: [
{
label: "Book for Tomorrow 6am",
type: "primary",
onClick: () => window.openai.callTool("book_class", {
classId: "hiit-123",
time: "2026-12-26T06:00:00Z"
})
},
{
label: "View Schedule",
type: "secondary",
onClick: () => window.openai.requestDisplayMode("fullscreen")
}
]
}
Why This Works:
- Title is clear and descriptive (under 60 chars)
- Metadata provides decision-critical info (duration, difficulty, equipment)
- Badge highlights new content
- Primary CTA is specific ("Book for Tomorrow 6am" vs "Book Now")
- Secondary CTA transitions to fullscreen for more complex browsing
Example 2: Restaurant Menu Card
// Production-ready restaurant inline card with carousel
const restaurantCarousel = {
type: "carousel",
items: restaurants.slice(0, 6), // 3-8 items per carousel
itemTemplate: (restaurant) => ({
title: restaurant.name,
metadata: [
`${restaurant.rating} stars • ${restaurant.cuisine}`,
`${restaurant.priceRange} • ${restaurant.distance}`
],
badge: restaurant.hasDiscount ? "20% Off" : null,
image: restaurant.thumbnail,
action: {
label: "View Menu",
onClick: () => window.openai.callTool("view_menu", {
restaurantId: restaurant.id
})
}
})
}
Why This Works:
- Carousel format for browsing multiple options
- 6 items (within 3-8 recommended range)
- Each card has single CTA (avoids overwhelming users)
- Metadata is concise (2 lines max)
- Badge highlights promotional offers
Key Takeaways
Optimizing ChatGPT inline cards requires balancing user experience constraints with technical implementation:
- Maximum 2 CTAs: Each inline card can display only 2 primary actions—design workflows to fit this constraint, not fight it
- No Nested Scrolling: Use carousels (max 8 items) instead of scrollable regions within cards
- Content Hierarchy: Prioritize title (60 chars) → metadata (2-3 lines) → description (300 chars) in that order
- Performance Target: Inline cards should render in under 200ms to maintain chat rhythm
- Keep structuredContent Concise: Well under 4k tokens for performance—use
_metafor large widget-only data - Action-Oriented CTAs: Labels should describe the outcome, not the interaction ("Schedule for Tomorrow 6am" vs "Submit")
Next Steps
Ready to build optimized inline cards that pass OpenAI review on first submission?
Explore MakeAIHQ Resources:
- ChatGPT App Templates - Pre-built inline card designs following these best practices
- Instant App Wizard - Generate compliant widget code automatically
- Widget Development Guide - Comprehensive guide to all display modes
Related Articles:
- window.openai API Reference — Technical implementation details for inline card widgets
- ChatGPT App Analytics & Optimization — Track inline card performance metrics
- Restaurant Table Reservations — Real-world inline card example for booking apps
- Content Recommendations for Media — Inline card patterns for content discovery
Start building your ChatGPT app today with MakeAIHQ's no-code platform—from zero to ChatGPT App Store in 48 hours.