CodeGym /Courses /ChatGPT Apps /Visual design: themes, colors, typography, spacing, and r...

Visual design: themes, colors, typography, spacing, and ready-made libraries

ChatGPT Apps
Level 8 , Lesson 3
Available

1. Context: your App is a guest in ChatGPT’s house

Before drawing buttons and picking fonts, accept the reality: the user is not opening “your website,” they are sitting inside ChatGPT. ChatGPT already has its own:

  • color scheme,
  • fonts and sizes,
  • spacing and element layout.

Your widget is rendered inside this environment, most often in an iframe. The important conclusion follows: visually, the App should feel like a natural extension of ChatGPT’s interface, not like a banner shipped in from 2008.

OpenAI’s official guidelines are exactly about this: don’t break system colors and fonts, add only moderate brand accents, and follow the platform’s basic typography and spacing grid.

Practically, this means three things.

First, background, base text color, and standard typography — all of this should be inherited from ChatGPT or system variables, not “I’m an artist, I see it this way.”

Second, if you want “your style,” it should be concentrated in accents: primary buttons, badges, highlighted states. Not a rainbow background and not a custom Comic Sans font — even if you really want to deep down.

Third, inline and fullscreen modes of the same App should visually be part of one world: same CTA colors, same card radii and spacing, same typography. The user shouldn’t feel like they landed in a different product when switching from inline to fullscreen.

Next we’ll break it down by layers: colors and themes, typography, spacing and grid, then how Tailwind and shadcn/ui help assemble it all.

Insight

The ChatGPT sandbox not only limits your widget’s functionality, it also applies its own styles.

First — the HTML root element

Original on your site:

<html lang="ru">

In the sandbox:

<html lang="en-US" data-theme="light" class="light" style="--safe-area-inset-top: 0px; --safe-area-inset-bottom: 0px; --safe-area-inset-left: 0px; --safe-area-inset-right: 0px;">

Second — built-in CSS styles to make your widget look more like ChatGPT:

<style>
  html,body,#root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin:0;padding:0}
  html,body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Helvetica Neue,Arial,sans-serif!important}
  button,input,textarea,select{font-family:inherit}
  html{background-color:#fff}
  html.dark{background-color:#212121}
  html.mobileSkybridge.dark{background-color:#000}
  @supports (font: -apple-system-body){html.mobileSkybridge{font:-apple-system-body}}
</style>

Better keep this in mind — there will be fewer surprises.

2. Themes and colors: living in light and dark worlds

Light and dark themes

The ChatGPT interface already supports light and dark themes. Your widget is rendered within one of them, and the user can switch at any time. That means any hardcoded white or black backgrounds are a potential landmine.

Imagine a widget that paints a white background and black text. In the light theme it looks tolerable. In the dark theme — like a spotlight in the eyes. The inverse is no better. That’s why the official recommendations advise against hardcoding colors and instead relying on the host’s theme/variables.

In Apps SDK environments you usually have an API or CSS variables for the current theme. The docs mention options like window.openai.theme and using ChatGPT’s standard CSS variables. Plus, there’s always prefers-color-scheme and the dark: utilities in Tailwind.

The idea is roughly this: your widget should automatically adapt to the host theme for things like:

  • card background (slightly lighter/darker than the base background),
  • text color (sufficient contrast),
  • borders, shadows, and hover states.

A simplest Tailwind wrapper for theme:

// components/AppShell.tsx
export function AppShell({ children }: { children: React.ReactNode }) {
  return (
    <div className="bg-background text-foreground">
      {/* bg-background/text-foreground are overridden by the theme */}
      {children}
    </div>
  );
}

Here, bg-background and text-foreground aren’t standard Tailwind classes, but aliases to your design system’s CSS variables (e.g., from shadcn/ui), which in turn are tied to ChatGPT’s light/dark theme.

System colors vs brand accents

OpenAI is quite clear: you can’t change ChatGPT’s system colors. Base text, standard chat panels, background — all that should remain in the platform’s shared palette. Your playground is accents inside the widget: CTA buttons (call to action — the primary action), badges, small elements.

In GiftGenius practice this means:

  • the gift card background is close to the system one,
  • text uses the default color, just like chat text,
  • the GiftGenius brand color is used for the primary “Choose a gift” button and perhaps a discount badge.

You can imagine a table:

Element Do Avoid
Widget background Inherit from ChatGPT Apply a loud branded gradient
Main text Inherit system color Make it colored/washed out to illegibility
Primary CTA button Use the brand accent color Paint a “rainbow” of 5 colors on it
Secondary buttons/links Keep close to system links Make them as loud as the CTA
Shadows/borders Subtle, minimal Thick neon outlines

Mini example with Tailwind for the primary color:

// styles/globals.css (fragment)
:root {
  --gift-accent: 222 84% 56%; /* hsl */
}

.dark {
  --gift-accent: 222 84% 64%; /* slightly lighter for dark */
}
// components/GiftButton.tsx
export function GiftButton({ children }: { children: React.ReactNode }) {
  return (
    <button className="rounded-md bg-[hsl(var(--gift-accent))] px-4 py-2 text-sm font-medium text-white hover:opacity-90">
      {children}
    </button>
  );
}

You don’t touch the widget’s overall background, but you gently apply your color to the primary CTA button.

Contrast and WCAG without going overboard

Even if you’re not cramming for a WCAG exam, here’s a simple yardstick: text must be readable. The smaller the font, the higher the required contrast. Accessibility courses recommend keeping text contrast relative to background at no less than ~4.5:1 for body text. We won’t dive deep into the standard here: we need one practical guideline — sufficient text/background contrast.

In practice:

  • don’t use light gray text on a light gray background for the sake of “elegance”;
  • avoid dark gray text on an almost-black background in dark mode;
  • at least eyeball it: if you’re squinting — your user will hurt too.

Make a pact with yourself: any secondary text (labels, hints) is still readable, just slightly less prominent in color and size — not “fully ghosted.”

3. Typography: system fonts, hierarchy, and common sense

System fonts instead of your own webfont

The official guidelines encourage using the platform’s system fonts like SF Pro, Roboto, and their equivalents, and not injecting your own webfont. The reason isn’t just performance — your App should look like a native part of the interface.

In a Next.js app, the easiest path is to make everything inside the widget inherit the base system stack. In Tailwind this is usually already set as font-sans. If you want to be explicit:

// app/layout.tsx (fragment)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className="font-sans antialiased">
        {children}
      </body>
    </html>
  );
}

No need to include three families via Google Fonts. For the training GiftGenius, a strict system font will look tidier than, say, Lobster.

Size hierarchy

We only need a few typographic levels: block title, subtitle/key attribute, body text, and caption.

For an inline GiftGenius card, for example, it’s convenient to settle on these levels:

Role Tailwind class Example
Card title
text-base font-semibold
Gift name
Key parameter
text-sm font-medium
Price or category
Description
text-sm text-muted-foreground
Short description
Caption/misc
text-xs text-muted-foreground
Shipping, store

Mini card component:

// components/GiftCard.tsx
type GiftCardProps = {
  title: string;
  price: string;
  description: string;
};

export function GiftCard({ title, price, description }: GiftCardProps) {
  return (
    <div className="rounded-lg border bg-card p-4">
      <h3 className="text-base font-semibold">{title}</h3>
      <p className="mt-1 text-sm font-medium text-emerald-600">{price}</p>
      <p className="mt-2 text-sm text-muted-foreground">{description}</p>
    </div>
  );
}

Here:

  • there’s no giant H1;
  • all information is compact;
  • hierarchy is conveyed by size and font weight.

Alignment and line length

Chat interfaces are usually narrow, especially inline. So there’s no need to overthink typography: left alignment and a line length of 40–60 characters are quite comfortable.

Good habits:

  • don’t center long card texts — they’re harder to read;
  • don’t write EVERYTHING IN ALL CAPS;
  • don’t make body text smaller than 14 px (in Tailwind that’s text-sm) without a very strong reason.

When in doubt, remember: the reader is a tired person on a phone in the subway, not you with a perfect 27-inch monitor.

4. Spacing, density, and grid

If colors and fonts are the “paints,” spacing is the air. Without it, even the neatest cards turn into mush.

OpenAI emphasizes in its recommendations: elements shouldn’t be “stuck together,” spacing and radii are best taken from a design system or UI framework (Tailwind, shadcn/ui, etc.), and horizontal scroll should be minimized.

The “let it breathe” principle

The simplest pattern: use a single spacing scale (e.g., a step of 4 px or 8 px) and don’t invent a “new size” every time. Tailwind has this built in: p-2, p-3, p-4, gap-3, etc.

Example of a small grid for the inline gift list:

// components/GiftListInline.tsx
export function GiftListInline({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex flex-col gap-3">
      {children}
    </div>
  );
}

Each card is separated by gap-3, has its own internal p-4, and that’s already enough to keep the list from looking like a “wall of text.”

Columns: inline vs fullscreen

UX docs for the Apps SDK recommend sticking to 1–2 card columns for an inline widget, and in fullscreen you can afford 2–3 if the width allows.

The reason is simple: chat width is limited, especially on mobile, and two columns are already borderline for readability. In fullscreen you get most of the screen and can lay out content more densely.

Approximate scheme:

flowchart LR
  subgraph Inline
    A[1 column
narrow screen] B[2 columns
on desktop] end subgraph Fullscreen C[2 columns
main scenario] D[3 columns
for grids/catalogs] end

Tailwind implementation for GiftGenius:

// components/GiftGrid.tsx
export function GiftGrid({ fullscreen, children }: { fullscreen?: boolean; children: React.ReactNode }) {
  const base = fullscreen ? "grid-cols-2 md:grid-cols-3" : "grid-cols-1 sm:grid-cols-2";

  return (
    <div className={`grid gap-4 ${base}`}>
      {children}
    </div>
  );
}

In inline mode, you give one column on mobile and two on wider screens. In fullscreen, you go straight to 2–3 columns depending on width.

Avoiding horizontal scroll

Chat is inherently vertical. The user is used to scrolling down, not sideways. Therefore:

  • try to make tables and cards fit the container width;
  • don’t set fixed widths like width: 600px; for an element living in a flexible container;
  • use max-w-full, overflow-x-auto only as a “last resort,” not by default.

For GiftGenius cards, it’s convenient to set w-full and let the grid decide how many fit per row.

5. Responsiveness inside the ChatGPT container

In regular frontend work you control the viewport. In ChatGPT that control is limited: your widget lives inside the chat container, with its own sizes and rules. The Apps SDK gives you a few helpful bridges: maximum height, safe area for notches, device type, etc.

maxHeight and vertical constraints

In inline mode, ChatGPT can limit the widget’s height so it doesn’t “eat” the whole screen. Hooks like useMaxHeight() let you know how much space you can fairly occupy right now and put internal scrollbars where needed.

Pseudocode:

// Pseudocode, not a real API:
const maxHeight = useMaxHeight();

return (
  <div style={{ maxHeight, overflowY: "auto" }}>
    <GiftGrid>{/* ... */}</GiftGrid>
  </div>
);

This helps you avoid a situation where the widget hits the bottom edge of the screen and chat messages “slide off into a past life.”

safeArea and mobile devices

On mobile devices there can be notches, a status bar, system panels. The Apps SDK lets you get the safeArea and adjust padding so nothing slips under the phone’s “notch.”

At the CSS level you can add extra padding:

// Pseudocode
const { top, bottom } = useSafeArea(); // say, returns { top: 8, bottom: 16 }

return (
  <div style={{ paddingTop: top, paddingBottom: bottom }}>
    {/* content */}
  </div>
);

For this lecture, the key principle is what matters: the widget should respect the height limiter and safe area, otherwise UX instantly becomes “scroll three more times to see the button.”

6. Tailwind and shadcn/ui: don’t reinvent buttons

Hand-coding all UI with raw CSS is almost a hardcore sport now. In the context of ChatGPT Apps it’s far easier to take a proven library and tune it to the platform’s requirements. In the course we rely on Tailwind and shadcn/ui as the base stack.

Tailwind as a dictionary of spacing and colors

Tailwind provides a handy set of utilities:

  • spacing (p-4, gap-3),
  • sizes (text-sm, text-base),
  • colors (text-muted-foreground, bg-card) which, in shadcn/ui and similar systems, are already tied to theme CSS variables.

This fits perfectly with ChatGPT’s requirements:

  • you don’t invent arbitrary spacing,
  • you set text sizes consistently,
  • you don’t break system colors — you use pre-agreed tokens.

shadcn/ui as a set of tidy components

shadcn/ui (and similar libraries) provides ready-made Card, Button, Input, Tabs, etc., tuned to the Tailwind theme. This significantly speeds up building a neat, minimalist interface, especially for GiftGenius cards.

GiftCard example using shadcn/ui:

// components/GiftCardShadcn.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";

type GiftCardProps = {
  title: string;
  price: string;
  description: string;
};

export function GiftCardShadcn(props: GiftCardProps) {
  return (
    <Card>
      <CardHeader>
        <CardTitle className="text-base">{props.title}</CardTitle>
      </CardHeader>
      <CardContent className="space-y-2">
        <p className="text-sm font-medium text-emerald-600">{props.price}</p>
        <p className="text-sm text-muted-foreground">{props.description}</p>
        <Button className="mt-2">Choose a gift</Button>
      </CardContent>
    </Card>
  );
}

The point here isn’t shadcn itself, but the principles:

  • the title isn’t gigantic;
  • the description is readable;
  • the button is styled by the shared design system, not “freehand.”

Tuning for ChatGPT

In a real project you can tune the palette to ChatGPT’s minimalist style: light background, soft shadows, neat radii. The module plan explicitly suggests leaning on an existing design system rather than creating your own universe.

A simple approach:

  • take the base shadcn/ui;
  • keep the system font;
  • configure one or two brand colors in primary / accent tokens;
  • make sure both inline and fullscreen use the same tokens.

This gives you a coherent visual core with minimal effort.

7. GiftGenius visual language: putting it all together

Let’s systematize what we can already consider the “visual language” for our hypothetical GiftGenius.

First, the color scheme. Background and text inherit from ChatGPT; the accent color is unobtrusive yet noticeable, applied to CTA buttons and possibly discount badges. In the dark theme this accent is slightly lighter to maintain contrast.

Second, typography. The base system font, text-sm for body text and text-base for card titles. Italics and caps are used sparingly, only when justified. Fullscreen wizard titles go one step up, but still nowhere near shouty text-4xl.

Third, spacing and grid. In inline mode the gift list is one or two columns with gap-3/gap-4, each card with p-4. In fullscreen — 2–3 columns, wizard steps with sufficient gaps between forms and buttons. No horizontal scroll for primary scenarios.

A small diagram for GiftGenius screens:

graph TD
  A[Inline: gift list] --> B[GiftCard
colors/typography/CTA] A --> C[GiftGrid 1–2 columns] D[Fullscreen: selection wizard] --> E[Step 1
form] D --> F[Step 2
filters/ranges] D --> G[Step 3
confirmation] B --> H[GiftButton
brand accent]

Fourth, compatibility with the host context. All elements behave nicely when switching light/dark themes, respect maxHeight, and don’t hide under the safe area. Colors don’t clash with ChatGPT, and CTA buttons look consistent everywhere so that muscle memory tells the user where to click.

This set of decisions already makes your app suitable for demo not only to engineers, but to real users or product managers: there will be something to discuss besides “here’s our MCP and here’s the Agents SDK.”

8. Accessibility (Accessibility Guidelines, WCAG AA)

We already briefly mentioned WCAG when talking about text/background contrast in section 2.3. There we cared about one practical yardstick — don’t kill readability. Now let’s look a bit wider: how the same interface appears to those who don’t see it with their eyes, and to ChatGPT itself in voice mode.

WCAG AA is an accessibility level from the international WCAG (Web Content Accessibility Guidelines) that describes how to make websites and interfaces accessible for people with various visual, motor, and cognitive limitations, etc.

The main idea of WCAG AA is to turn an interface from merely “theoretically accessible” into truly usable. This level includes dozens of requirements that directly affect interaction quality. Among them is that same text/background contrast threshold around 4.5:1 we already mentioned, as well as requirements for clickable area sizes, focus states, form error handling, etc.

Another layer is support for assistive technologies, including screen readers. The AA level requires correct semantics: headings must be headings, lists must be lists, buttons must be buttons, and interactive elements must have proper roles and text alternatives. This allows users working with VoiceOver, TalkBack, or NVDA to fully understand the structure and meaning of the interface.

Screen reader

A screen reader is software that speaks and/or structures the on-screen content, allowing people with visual impairments to use a computer, smartphone, or web apps.

But a screen reader isn’t just “a program that reads text aloud.” It’s a full interaction system that turns the visual representation of a site or app into accessible audio and structured navigation.

ChatGPT, screen reader, and WCAG AA

If your widget is marked up according to WCAG AA principles (correct roles, headings, button labels), it becomes understandable not only to screen readers, but also to ChatGPT in voice mode. The user speaks to ChatGPT, and the model, relying on the same semantic structure, can “virtually” do what a person would: find needed interface elements, click buttons, follow links, etc.

According to ChatGPT Store requirements, support for the WCAG AA standard is mandatory for every application. Every widget and every tool should have the richest possible descriptions, and the layout should comply with WCAG AA standards: correct semantics, readable labels, predictable states.

Therefore, the WCAG AA requirement isn’t a separate “feature for people with special needs,” but a fundamental design principle so that ChatGPT Apps can fully operate your application, including when the user interacts via voice.

We’ll return to voice-UX scenarios, differences between voice and text dialogue, and ChatGPT Store requirements separately — in other lessons of this module and in the module about publishing your App. But all of this stands on the foundation you’ve just seen: voice mode = multimodality + accessibility (WCAG AA + screen readers).

9. Common visual design mistakes for a ChatGPT App

Mistake No. 1: Hardcoded white/black backgrounds and text colors.
A developer paints a white background and black text without considering dark mode. It barely survives in light mode, and in dark it becomes a spotlight and ruins UX. It’s better to use system colors and the host theme (CSS variables, prefers-color-scheme, or the Apps SDK API) and keep your own colors only for accents.

Mistake No. 2: Overly aggressive branding.
A loud gradient background appears, a custom font, flashy borders. The widget starts to look like a promo banner, not part of ChatGPT’s interface. The guidelines require the opposite: a minimalist, “native” look with tasteful use of brand color only in key elements, like primary buttons.

Mistake No. 3: No typographic hierarchy.
All texts are the same size and weight — or the opposite, three levels of headings on a small card, and in caps. The user can’t tell what’s primary: the name, the price, or the description. It’s better to agree upfront on 3–4 levels and stick to them: title, key parameter, body text, caption.

Mistake No. 4: Elements crammed together without spacing.
Cards touch each other, text hugs the edge, buttons are flush with text. On desktop it’s barely tolerable; on mobile it turns into visual noise. Use a single spacing scale (e.g., Tailwind classes p-4, gap-3) and don’t skimp on air.

Mistake No. 5: Trying to squeeze 4–5 columns into inline mode.
The developer is still mentally on an e-commerce page and builds a four-column tile in chat. On wide screens it’s questionable; on mobile it’s unreadable, and horizontal scroll appears. In inline widgets, one or two columns usually suffice; leave the third column to fullscreen mode.

Mistake No. 6: Ignoring height limits and the safe area.
The widget renders a gigantic list without internal scrolling and without respecting maxHeight, so buttons end up “below the bottom of the screen.” Or elements hide under the notch on mobile. Use the maximum height and safe area data to distribute height and padding correctly inside.

Mistake No. 7: Inconsistent look of buttons and cards between inline and fullscreen.
In inline, the button is green and rounded; in fullscreen — blue and square. The user loses the sense of a single product. Move basic button and card styles into a shared component/theme and use them in all modes.

Mistake No. 8: A “signature” font and decorative flourishes.
Adding a heavy webfont “for beauty” breaks visual consistency with ChatGPT and sometimes hurts performance. The platform’s recommendations say to use system fonts and tidy typography. If you really want to express your design taste — work on icons and microcopy, not a font revolution.

1
Task
ChatGPT Apps, level 8, lesson 3
Locked
Theme-aware Surface + one branded accent (very easy)
Theme-aware Surface + one branded accent (very easy)
1
Task
ChatGPT Apps, level 8, lesson 3
Locked
Typographic scale and card 'density' (easy)
Typographic scale and card 'density' (easy)
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION