1. コンテキスト: あなたの App は ChatGPT の家に招かれたゲスト
ボタンを描いたりフォントを選んだりする前に、現実を受け入れましょう。ユーザーは「あなたのサイト」を開いているのではなく、ChatGPT を使っています。ChatGPT にはすでに次のものがあります:
- 配色、
- フォントとサイズ、
- 余白とレイアウト。
あなたのウィジェットは多くの場合 iframe 内で、この環境の中に表示されます。重要な結論はこうです。App は、ChatGPT のインターフェースの自然な延長として見えなければならず、2008 年から運ばれてきたバナーのように見えてはいけません。
OpenAI の公式ガイドラインもまさにこれを求めています。システムの色やフォントを壊さず、控えめなブランドアクセントだけを加え、プラットフォームの基本的なタイポグラフィとグリッドに従うこと。
実務的には、これは次の3点に集約されます。
第一に、背景、基本のテキスト色、標準的なタイポグラフィなどは、ChatGPT やシステム変数から継承し、「アーティスト的な自己流」で決めないこと。
第二に、「自分のスタイル」を入れるならアクセントに集中させること。主要ボタン、バッジ、強調状態などです。レインボーの背景や Comic Sans のカスタムフォントはやめましょう——たとえ心の中では強く望んでいても。
第三に、同じ App の inline と fullscreen の両モードが、同じ世界の一部として視覚的に統一されていること。CTA の色、カードの角丸と余白、タイポグラフィが同じであるべきです。inline から fullscreen に移っても、別プロダクトに来たと感じさせないようにします。
続いて層ごとに見ていきます。色とテーマ、タイポグラフィ、余白とグリッド、そして Tailwind と shadcn/ui がそれらをどう組み立てるのに役立つか。
インサイト
ChatGPT のサンドボックスは、ウィジェットの機能を制限するだけでなく、スタイルも付与します。
まず — これは HTML のルート要素です。
サイトのオリジナル:
<html lang="ru">
サンドボックス内:
<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;">
次に — これは組み込みの CSS スタイルで、あなたのウィジェットをより 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>
この点を覚えておくと、驚きが減ります。
2. テーマと色: ライトとダーク、二つの世界で生きる
ライトテーマとダークテーマ
ChatGPT のインターフェースは、すでにライトとダークの両テーマをサポートしています。あなたのウィジェットはそのどちらかの中に表示され、ユーザーはいつでも切り替えることができます。つまり、白や黒の背景をハードコードするのは潜在的な地雷です。
白背景・黒テキストを描くウィジェットを想像してください。ライトテーマではまだ許容範囲でも、ダークテーマでは目にスポットライトです。逆に、ライトテーマで黒背景というのも良くありません。だからこそ公式の推奨は、色をハードコードせず、ホストのテーマ/変数に依存することなのです。
Apps SDK の環境では、現在のテーマのための API や CSS 変数が用意されていることが多いです。ドキュメントには window.openai.theme のような例や、ChatGPT の標準 CSS 変数の使用が見られます。さらに prefers-color-scheme や Tailwind の dark: ユーティリティも使えます。
考え方はこうです。ウィジェットはホストのテーマに合わせて、自動的に次のような点を調整できるべきです:
- カードの背景(ベースより少し明るい/暗い)、
- テキスト色(十分なコントラスト)、
- ボーダー、影、hover 状態。
Tailwind を使った最小のテーマラッパー例:
// components/AppShell.tsx
export function AppShell({ children }: { children: React.ReactNode }) {
return (
<div className="bg-background text-foreground">
{/* bg-background/text-foreground はテーマで上書きされる */}
{children}
</div>
);
}
ここでの bg-background と text-foreground は標準の Tailwind クラスではなく(例: shadcn/ui 由来の)デザインシステムの CSS 変数へのエイリアスで、裏では ChatGPT のライト/ダークテーマに紐づいています。
システムカラーとブランドアクセントの棲み分け
OpenAI はかなり明確に述べています。ChatGPT のシステムカラーは変更してはいけない、と。基本のテキスト、標準のチャットパネル、背景——これらはプラットフォームの共通色のままであるべきです。あなたの領域はウィジェット内のアクセントです。CTA(call to action)ボタン、バッジ等の小さな要素です。
GiftGenius の実装では、これは次を意味します:
- ギフトカードの背景はシステムに近い色、
- テキストはチャットと同じく標準色、
- GiftGenius のブランドカラーは、メインの「ギフトを選ぶ」ボタンや、場合によっては割引バッジに使用。
イメージしやすい表:
| 要素 | やるべきこと | 避けるべきこと |
|---|---|---|
| ウィジェットの背景 | ChatGPT から継承 | 派手なブランドグラデーションを敷く |
| 基本テキスト | システム色を継承 | 読めないほどの色付き/薄いグレーにする |
| メインの CTA ボタン | ブランドのアクセントカラーを使う | 「虹」や 5 色を塗る |
| セカンダリボタン/リンク | システムのリンクに近く | CTA と同じくらい派手にする |
| 影/ボーダー | 控えめでミニマル | 太いネオンの縁取り |
メインカラーの Tailwind ミニ例:
// styles/globals.css(抜粋)
:root {
--gift-accent: 222 84% 56%; /* hsl */
}
.dark {
--gift-accent: 222 84% 64%; /* 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>
);
}
ウィジェット全体の背景には触れず、メインの CTA ボタンにのみ自分たちの色を丁寧に適用します。
コントラストと WCAG(過度にならない範囲で)
WCAG の試験を受けるつもりがなくても、シンプルな指針があります。テキストは読めるべきだ、と。フォントが小さくなるほど、コントラストは高く必要です。アクセシビリティの講座では、本文テキストの背景に対するコントラストをおよそ 4.5:1 以上に保つことが推奨されます。ここでは標準の詳細には踏み込みません。実用上の目安はひとつ——テキストと背景の十分なコントラストです。
実務上の注意:
- 「上品さ」のために薄いグレーの文字を薄いグレーの背景に載せない;
- ダークテーマでほぼ黒の背景に濃いグレーの文字を載せない;
- 最低限の目視チェックを。目を細めるなら、ユーザーもつらいはずです。
自分と約束しましょう。補助的なテキスト(キャプション、ヒント)でも、あくまで読みやすく。色とサイズを少し抑えるだけで、「幽霊文字」にしないこと。
3. タイポグラフィ: システムフォント、階層、そして健全な常識
「自前」フォントではなくシステムフォントを
公式ガイドラインは、SF Pro や Roboto といったプラットフォームのシステムフォントを使い、独自の webfont を持ち込まないよう求めています。理由はパフォーマンスだけでなく、App をインターフェースのネイティブな要素に見せるためでもあります。
Next.js アプリで、ウィジェット内のすべてがベースのシステムスタックを継承するようにするのが簡単です。Tailwind では通常 font-sans が既に設定されています。さらに明示したいなら:
// app/layout.tsx(抜粋)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="font-sans antialiased">
{children}
</body>
</html>
);
}
Google Fonts から 3 ファミリーを読み込む必要はありません。学習用の GiftGenius では、厳格なシステムフォントのほうが、たとえば Lobster のようなものよりずっと整って見えます。
サイズの階層
必要なのは数段階のタイポグラフィだけです。ブロックの見出し、副見出し/主要パラメータ、本文、キャプション。
たとえば GiftGenius の inline カードでは、次のレベルにしておくと便利です:
| 役割 | Tailwind クラス | 例 |
|---|---|---|
| カードの見出し | |
ギフト名 |
| 主要なパラメータ | |
価格やカテゴリ |
| 説明 | |
短い説明文 |
| 注記/細部 | |
配送、ショップ |
カードのミニコンポーネント:
// 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>
);
}
ここでは:
- 巨大な H1 はありません;
- 情報はコンパクトです;
- サイズとウェイトの違いで階層が伝わります。
整列と行長
チャット UI は通常、特に inline では横幅が狭いです。ですから、複雑なタイポグラフィで悩む必要はありません。左揃えの通常文で、40〜60 文字程度の行長が十分に快適です。
実用的な心得:
- カード内の長文を中央揃えにしない——読みづらくなります;
- 全文字を大文字にしない;
- 強い理由がない限り、ベースの文字サイズを 14 px(Tailwind なら text-sm)より小さくしない。
迷ったら思い出してください。読むのは地下鉄のスマホで疲れた人であって、あなたの理想的な 27 インチモニターではありません。
4. 余白、密度、グリッド
色とフォントが「絵の具」なら、余白は空気です。空気がないと、どんなにきれいなカードも台無しです。
OpenAI の推奨では、要素は「くっついて」いてはいけない、余白や角丸はデザインシステムや UI フレームワーク(Tailwind、shadcn/ui など)から取るのがよい、そして横スクロールは最小限に、という点が強調されています。
「呼吸する」原則
最も簡単なパターンは、統一された余白スケール(たとえば 4 px や 8 px 刻み)を使い、毎回「自分流のサイズ」を発明しないこと。Tailwind には既に p-2、p-3、p-4、gap-3 などが用意されています。
inline のギフト一覧用の小さなグリッド例:
// components/GiftListInline.tsx
export function GiftListInline({ children }: { children: React.ReactNode }) {
return (
<div className="flex flex-col gap-3">
{children}
</div>
);
}
各カードは gap-3 で区切られ、内側に p-4 を持ちます。これだけで、一覧が「長い巻物」のように見えるのを防げます。
カラム: inline と fullscreen の違い
Apps SDK の UX ドキュメントでは、inline ウィジェットでは 1〜2 列、fullscreen では十分な幅があるなら 2〜3 列を推奨しています。
理由は単純です。チャットは横幅が限られており、特にモバイルでは 2 列でも可読性のギリギリです。fullscreen なら画面をほぼ丸ごと使えるので、より密に配置できます。
おおまかな図:
flowchart LR
subgraph Inline
A[1 列
狭い画面]
B[2 列
デスクトップ]
end
subgraph Fullscreen
C[2 列
基本シナリオ]
D[3 列
グリッド/カタログ向け]
end
GiftGenius の Tailwind 実装例:
// 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>
);
}
inline ではモバイルで 1 列、広い画面で 2 列。fullscreen では幅に応じて最初から 2〜3 列にします。
水平スクロールを避ける
チャットは本質的に縦スクロールです。ユーザーは横ではなく下方向にスクロールすることに慣れています。したがって:
- テーブルやカードはコンテナの幅に収まるようにする;
- 柔軟なコンテナに住む要素に width: 600px; のような固定幅を与えない;
- max-w-full や overflow-x-auto は「最後の手段」であり、デフォルトにしない。
GiftGenius のカードには w-full を与え、横に何枚並ぶかはグリッドに任せるのが便利です。
5. ChatGPT コンテナ内でのレスポンシブ対応
通常のフロントエンドでは viewport を完全に制御できますが、ChatGPT では制約があります。ウィジェットはチャットのコンテナ内にあり、独自のサイズやルールを持ちます。Apps SDK は、最大高さ、safe area、デバイス種別などいくつかの便利な橋渡しを提供します。
maxHeight と縦方向の制約
inline モードでは、ウィジェットが画面全体を「食べない」ように、ChatGPT が高さを制限することがあります。useMaxHeight() のようなフックで、現在正当に使えるスペースを知り、必要な場所に内側スクロールを付けられます。
擬似コード:
// 擬似コード。実際の API ではありません:
const maxHeight = useMaxHeight();
return (
<div style={{ maxHeight, overflowY: "auto" }}>
<GiftGrid>{/* ... */}</GiftGrid>
</div>
);
これで、ウィジェットが画面下端に突っかかり、チャットメッセージが「過去のどこか」に押しやられる事態を避けられます。
safeArea とモバイルデバイス
モバイルでは、上部と下部にノッチ、ステータスバー、システムパネルがあることがあります。Apps SDK では safeArea を取得して、何もノッチの下に隠れないようにパディングを調整できます。
CSS レベルでは追加のパディングを加えられます:
// 擬似コード
const { top, bottom } = useSafeArea(); // たとえば { top: 8, bottom: 16 } を返す
return (
<div style={{ paddingTop: top, paddingBottom: bottom }}>
{/* コンテンツ */}
</div>
);
この講義では原理が重要です。ウィジェットは最大高さの制限と安全領域を尊重すべきで、そうでないと UX は瞬時に「ボタンを見るためにさらに 3 回スクロールする」ものになってしまいます。
6. Tailwind と shadcn/ui: ボタンを再発明しない
今どき、UI を生の CSS で手書きするのはほとんどハードコアスポーツです。ChatGPT Apps の文脈では、実績あるライブラリを取り入れて、プラットフォームの要件に合わせて調整したほうが楽です。このコースでは、Tailwind と shadcn/ui を基本スタックとして使います。
Tailwind を余白と色の辞書として使う
Tailwind は便利なユーティリティを提供します:
- 余白(p-4、gap-3)、
- サイズ(text-sm、text-base)、
- 色(text-muted-foreground、bg-card)。shadcn/ui などのシステムでは、これらはテーマの CSS 変数に既に紐づいています。
これは ChatGPT の要件と非常に相性が良いです:
- 恣意的な余白を発明しない;
- テキストサイズを一貫して指定できる;
- システムカラーを壊さず、事前に合意されたトークンを使える。
shadcn/ui を整ったコンポーネントセットとして使う
shadcn/ui(や類似ライブラリ)は、Card、Button、Input、Tabs といったコンポーネントを提供し、Tailwind テーマに合わせてあります。特に GiftGenius のカードのような、整ったミニマル UI の構築を大幅に加速できます。
shadcn/ui を使った GiftCard の例:
// 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">ギフトを選ぶ</Button>
</CardContent>
</Card>
);
}
ここで重要なのは shadcn そのものではなく、以下の原則です:
- 見出しは巨大にしない;
- 説明は読みやすい;
- ボタンは独自流ではなく、共通のデザインシステムに沿っている。
ChatGPT に合わせた調整
実案件では、ChatGPT のミニマルなスタイルに合わせてパレットを調整できます。明るい背景、柔らかい影、控えめな角丸。モジュールの計画は、独自宇宙を作るのではなく、既存のデザインシステムに依拠することを勧めています。
シンプルな方針:
- ベースは shadcn/ui を採用;
- システムフォントのままにする;
- primary / accent のトークンで 1〜2 色のブランドカラーを設定;
- inline と fullscreen のどちらも同じトークンを使うことを確認。
これで余計な労力なく、一貫したビジュアルコアが手に入ります。
7. GiftGenius のビジュアル言語: すべてを統合する
ここまでで、私たちの仮想アプリ GiftGenius の「ビジュアル言語」と呼べるものを整理しましょう。
第一に、配色。背景とテキストは ChatGPT から継承。アクセントカラーは控えめながら視認性があり、CTA ボタンや場合により割引バッジに適用。ダークテーマではコントラスト維持のため少し明るめに。
第二に、タイポグラフィ。ベースはシステムフォント。本文は text-sm、カードの見出しは text-base。イタリックやオールキャップスは稀に、必要なときだけ。fullscreen の見出しは一段大きくしますが、text-4xl のように叫びません。
第三に、余白とグリッド。inline のギフト一覧は 1〜2 列で gap-3/gap-4、各カードは p-4。fullscreen では 2〜3 列、ウィザードの各ステップ間にも十分なスペースを。主要シナリオでは横スクロールなし。
GiftGenius の画面イメージの図:
graph TD A[Inline: ギフト一覧] --> B[GiftCard
カラー/タイポグラフィ/CTA] A --> C[GiftGrid 1〜2 列] D[Fullscreen: 選定ウィザード] --> E[ステップ1
フォーム] D --> F[ステップ2
フィルター/範囲] D --> G[ステップ3
確認] B --> H[GiftButton
ブランドアクセント]
第四に、ホスト文脈との両立。ライト/ダークの切り替えで全要素が適切にふるまい、maxHeight を尊重し、safe-area の下に隠れません。色は ChatGPT と衝突せず、どこでも同じ見え方の CTA ボタンにより、ユーザーは筋肉記憶で押す場所を理解できます。
このセットがあれば、あなたのアプリはエンジニアだけでなく実ユーザーやプロダクトマネージャーにも見せられる品質になります。「ここは MCP、あそこは Agents SDK」といった話だけではなく、具体的な UI/UX を議論できます。
8. アクセシビリティ(Accessibility Guidelines, WCAG AA)
2.3 節でテキストと背景のコントラストの話をするときに WCAG に触れました。実用上の目安はひとつ——可読性を殺さないこと。ここでは視野を広げ、画面を目で見ない人や、ChatGPT の音声モードにとって同じインターフェースがどう見えるのかを見ていきます。
WCAG AA は、国際的な規格 WCAG (Web Content Accessibility Guidelines) の中の水準で、視覚、運動、認知などに多様な制約を持つ人々に対して、サイトやインターフェースをアクセシブルにする方法を定めています。
WCAG AA の主眼は、インターフェースを単なる「理論上アクセシブル」から、実際に使えるものにすることです。このレベルは、ユーザー体験に直接影響する多くの要件を含みます。先ほど述べたテキストと背景のコントラストおよそ 4.5:1 のしきい値のほか、クリック可能領域のサイズ、フォーカス状態、フォームのエラーなどが含まれます。
もうひとつの層が、支援技術のサポートです。スクリーンリーダーを含みます。AA レベルでは正しいセマンティクスが求められます。見出しは見出し、リストはリスト、ボタンはボタン。インタラクティブ要素には適切なロールや代替テキストが必要です。これにより、VoiceOver、TalkBack、NVDA といったスクリーンリーダー利用者が、構造と意味を十分に理解できます。
スクリーンリーダー(screen reader)
スクリーンリーダー(screen reader)は、画面の内容を音声化し、あるいは構造化して提示するプログラムで、視覚に障害のある人が PC、スマートフォン、ウェブアプリを利用できるようにします。
ただの「テキストを読み上げるソフト」ではありません。サイトやアプリの視覚的表現を、音と構造化されたナビゲーションに変換する、完全なインタラクションシステムです。
ChatGPT、スクリーンリーダー、そして WCAG AA
ウィジェットが WCAG AA の原則(正しいロール、見出し、ボタンのラベル)でマークアップされていれば、スクリーンリーダーだけでなく、ChatGPT の音声モードにも理解しやすくなります。ユーザーは ChatGPT と音声で対話し、モデルは同じセマンティック構造に基づいて、人間が行うのと同様に「仮想的に」UI 要素を見つけ、ボタンを押し、リンクへ移動できます。
ChatGPT Store の要件によれば、WCAG AA のサポートは各アプリにとって必須です。各ウィジェットや各 tool は、可能な限り質の高い詳細な説明を持ち、マークアップは WCAG AA に準拠しているべきです。正しいセマンティクス、読みやすいラベル、予測可能な状態。
ですから WCAG AA は、「特別なニーズのある人のための追加機能」ではなく、ChatGPT Apps があなたのアプリと完全に連携するための基本的なデザイン原則です。音声モードでユーザーが対話する場合も含めて。
音声 UX のユースケース、音声ダイアログとテキストの違い、そして ChatGPT Store の要件については、このモジュールの他のレッスンや App の公開モジュールで改めて扱います。しかし、その土台は今見たとおりです。音声モード = マルチモーダル + アクセシビリティ(WCAG AA + スクリーンリーダー)。
9. ChatGPT App のビジュアルデザインでよくある誤り
エラー 1: 白/黒の背景とテキスト色をハードコードする。
開発者が白背景・黒文字を固定し、ダークテーマを考慮しない。ライトテーマではまだしも、ダークではスポットライトとなり UX を台無しにします。正しくはシステムカラーとホストのテーマ(CSS 変数、prefers-color-scheme、あるいは Apps SDK の API)を使い、ブランド色はアクセントに限定すること。
エラー 2: ブランディングが強すぎる。
派手なグラデ背景、カスタムフォント、カラフルなボーダー。ウィジェットが ChatGPT の一部ではなく、プロモバナーのように見えてしまいます。ガイドラインはその逆を求めています。ミニマルで「ネイティブ」な見た目。ブランド色は主要要素、たとえばメインボタンに控えめに使います。
エラー 3: タイポグラフィに階層がない。
すべてが同じサイズ/ウェイト、あるいは小さなカードに 3 種類の見出しが大文字で並ぶ。ユーザーは何が主なのか(名前、価格、説明)を理解できません。3〜4 段階をあらかじめ決め、見出し、主要パラメータ、本文、注記を一貫して使いましょう。
エラー 4: 余白がなく要素が密着している。
カードが互いに接し、テキストが端にぴったり、ボタンがテキストにくっついている。デスクトップではまだ耐えられても、モバイルでは視覚的ノイズになります。統一スケールの余白(たとえば Tailwind の p-4、gap-3)を使い、空気をケチらないことを推奨します。
エラー 5: inline モードに 4〜5 列を押し込む。
開発者の頭はまだ EC サイトの一覧ページにあり、チャットに 4 枚の細長いカードを並べます。広い画面でも微妙、モバイルでは読めたものではなく、水平スクロールも生まれます。inline では通常 1〜2 列で十分。3 列目は fullscreen に譲りましょう。
エラー 6: 高さの制限と safe-area を無視する。
ウィジェットが内側スクロールも maxHeight も考慮せず巨大リストを描き、ボタンが「画面の底より下」に行ってしまう。あるいは要素がモバイルのノッチに隠れる。最大高さと安全領域の情報を用いて、内側の高さやパディングを適切に配分しましょう。
エラー 7: inline と fullscreen でボタンとカードの見え方がバラバラ。
inline ではボタンが緑で角丸、fullscreen では青で角張っている。ユーザーは単一プロダクトの感覚を失います。ボタンとカードの基本スタイルは共通のコンポーネント/テーマに切り出し、あらゆるモードで同じものを使ってください。
エラー 8: 「作家性」の強いフォントと装飾的ひねり。
「映えるから」と重い webfont を入れると、ChatGPT とのビジュアル一貫性が壊れ、パフォーマンスを損なうことも。プラットフォームの推奨はシステムフォントと控えめなタイポグラフィです。どうしてもデザイナーとして表現したいなら、フォント革命ではなくアイコンやマイクロコピーに工夫を向けましょう。
GO TO FULL VERSION