CodeGym /Cursos /ChatGPT Apps /Controle da aparência: dis...

Controle da aparência: displayMode, maxHeight, borders, theme, layout

ChatGPT Apps
Nível 3 , Lição 1
Disponível

1. Por que gerenciar a aparência

Agora seu widget provavelmente parece um “componente React normal”: um div, uma lista de itens, alguns botões. Na web comum isso muitas vezes basta. No ChatGPT, porém, há um detalhe: sua UI vive dentro do chat, onde o usuário já tem muito contexto visual — mensagens, outros Apps, interface de voz, além de limitações no tamanho do contêiner.

É importante lembrar de duas coisas.

Em primeiro lugar, o widget tem um modo de exibição (displayMode): inline, fullscreen, às vezes PiP. O modo determina a área disponível, o comportamento do scroll e as expectativas do usuário.

Em segundo lugar, a plataforma informa ao widget as restrições de altura (maxHeight) e o tema (theme). Se você ignorá-los e desenhar algo do tamanho do Notion dentro de uma única mensagem, o chat vira um “buraco negro”, onde tudo se perde dentro de um enorme iframe. A OpenAI recomenda explicitamente manter uma UI concisa e respeitar as cores/tipografia do sistema.

Um cenário típico do GiftGenius ilustra bem como isso funciona na prática. O usuário pede: “Encontre um presente para um amigo de até US$ 50”. O ChatGPT inicia o GiftGenius, que no modo inline mostra cards compactos de presentes e alguns botões. O usuário clica em “Detalhes” — o widget solicita fullscreen e aí mostra filtros, descrição detalhada, avaliações. Quando o pagamento está em andamento, é possível mostrar um pequeno PiP/modal com o status “Processando o pedido…”, sem cobrir todo o chat.

Nosso objetivo nesta aula é aprender a:

  • entender qual é o displayMode atual e se comportar de forma adequada;
  • alternar o modo sob demanda (inline ↔ fullscreen, às vezes PiP);
  • respeitar maxHeight e evitar o “scroll duplo”;
  • adaptar estilos ao tema claro/escuro e à largura da tela;
  • construir um layout que pareça “nativo” dentro do ChatGPT.

2. Modos de displayMode: inline, fullscreen, PiP

Vamos começar pelos conceitos. displayMode é o estado do contêiner do seu widget no ChatGPT. Ele vem da plataforma (via window.openai.displayMode ou hook useDisplayMode) e pode assumir valores como "inline", "fullscreen", "pip".

Inline

Inline é o modo padrão. O widget é inserido diretamente no fluxo de mensagens como mais um “bloco” entre respostas de texto. A largura é limitada pela coluna do chat (no desktop ~700–800px, no celular — a largura da tela), e a altura é dinâmica, mas não infinita.

Inline é ideal para:

  • apresentações curtas e autoexplicativas: cards de presentes, lista de opções, resumo de busca;
  • uma ou duas ações: “Selecionar”, “Cancelar”, “Mostrar mais”.

Para o GiftGenius esse é o modo principal: o usuário faz o pedido e você mostra 3–5 cards de presentes com botões, sem tomar a tela inteira.

Fullscreen (Canvas)

Fullscreen (ou canvas) é o modo em que seu widget ocupa a maior parte da área visível. O chat não desaparece: a caixa de entrada ainda está disponível, mas a atenção principal fica na sua UI.

Ativar o fullscreen faz sentido quando:

  • há muitos campos de entrada ou um assistente complexo (checkout, filtros avançados, configurações);
  • é preciso mostrar grandes tabelas, mapas, comparação de dezenas de itens;
  • o inline já não comporta e começa a parecer um mini Excel com 700px de altura.

No GiftGenius, o fullscreen serve para dar ao usuário filtros completos, ordenação, descrições detalhadas, possivelmente várias abas.

PiP / Modal

PiP (picture-in-picture) e modais são pequenas janelas “flutuantes” acima do conteúdo principal. Nas implementações atuais do Apps SDK, o PiP costuma ser implementado como um modo especial de displayMode ou como janela modal via requestModal().

Eles são úteis quando:

  • é preciso mostrar o status de um processo demorado (processamento de pedido, renderização de vídeo);
  • é necessário perguntar algo pequeno sem interromper o fluxo principal (confirmação rápida);
  • você quer dar ao usuário a possibilidade de “manter o widget à vista” enquanto continua no chat.

No GiftGenius isso pode ser um pequeno painel “Processando o pedido… 30 %” com um botão “Cancelar”.

Pequena comparação

Tabela para percepção visual:

Modo Onde fica Casos típicos Restrições
inline
no fluxo de mensagens Listas, cards, um ou dois botões Altura limitada, largura estreita
fullscreen
sobre o chat / lateral Assistentes, formulários complexos, tabelas Exige layout e navegação bem pensados
PiP / modal camada flutuante Status, mini‑formulários, vídeo Pouquíssimo espaço; tudo deve ser grande e simples

É importante não tratar o fullscreen como “aplicativo de verdade” e o inline como “preview”. É o mesmo App, apenas em “poses” diferentes.

3. Hooks para trabalhar com o modo: useDisplayMode, useRequestDisplayMode, useRequestModal

Agora que entendemos o que são inline/fullscreen/PiP do ponto de vista de UX, vamos ver como trabalhar com eles no código via hooks do Apps SDK.

Em vez de ler window.openai.displayMode diretamente, usamos um hook do template, que assina mudanças e poupa você dos rituais com eventos do SDK. Uma interface típica seria:

// tipos fictícios; confirme os nomes reais no template
type DisplayMode = 'inline' | 'fullscreen' | 'pip';

function useDisplayMode() {
  // retorna o modo atual
  return { displayMode: 'inline' as DisplayMode };
}

function useRequestDisplayMode() {
  // função para solicitar a troca de modo
  return {
    requestDisplayMode: (mode: DisplayMode) => {
      /* chama window.openai.requestDisplayMode */
    },
  };
}

Vamos criar um componente simples que mostra o modo atual e fornece um botão “Expandir / Recolher”:

import { useDisplayMode, useRequestDisplayMode } from '@/apps-sdk';

export function DisplayModeDebug() {
  const { displayMode } = useDisplayMode();
  const { requestDisplayMode } = useRequestDisplayMode();

  const toggle = () => {
    requestDisplayMode(displayMode === 'inline' ? 'fullscreen' : 'inline');
  };

  return (
    <div className="text-xs text-gray-500 flex gap-2 items-center">
      <span>Modo: {displayMode}</span>
      <button onClick={toggle} className="underline">
        Alternar
      </button>
    </div>
  );
}

Em Apps reais você normalmente esconde esses elementos “de debug”, mas no Dev Mode tal componente ajuda bastante a sentir como o widget se comporta ao alternar.

Inline vs Fullscreen com subcomponentes diferentes

Um erro comum é tentar atender todos os modos com o mesmo layout e encher o JSX de if (displayMode === ...). É muito mais fácil dividir a apresentação:

import { useDisplayMode } from '@/apps-sdk';
import { GiftListInline } from './GiftListInline';
import { GiftListFullscreen } from './GiftListFullscreen';

export function GiftWidget() {
  const { displayMode } = useDisplayMode();

  if (displayMode === 'fullscreen') {
    return <GiftListFullscreen />;
  }

  return <GiftListInline />;
}

Assim o código se lê como “se for fullscreen — aqui vai o assistente complexo; caso contrário — o inline compacto”. E cada subcomponente pode ser estilizado separadamente de acordo com suas restrições. Essa abordagem é justamente a recomendada: separar os modos em subcomponentes em vez de um enorme if/else em um único componente.

Modais: useRequestModal

Se o template fornece o hook useRequestModal, sua interface geralmente é parecida com:

const { requestModal } = useRequestModal();
// requestModal({ title }) ou algo do tipo.

Modais lembram o fullscreen em alguns aspectos, mas não o substituem: fullscreen é para cenários grandes; modal é para um passo curto (confirmar uma ação, inserir um cupom etc.).

4. Controle de tamanho: maxHeight, scroll e notifyIntrinsicHeight()

O segundo eixo importante é a altura. A plataforma informa ao widget: “Esta é a altura máxima disponível”. Esse limite pode ser lido em window.openai.maxHeight ou via hook useMaxHeight.

Por que não definir simplesmente “height: 5000px”

Se você ignorar o maxHeight e definir uma altura fixa enorme, o ChatGPT será obrigado a cortar seu conteúdo. Ou dará ao usuário um scroll duplo: o externo — do chat, e o interno — do seu widget. Isso é um UX ruim: o usuário precisa adivinhar onde exatamente rolar para alcançar o botão certo.

A estratégia correta é:

  1. Ler o limite maxHeight.
  2. Construir o layout de forma que o scroll principal permaneça no chat (especialmente no inline).
  3. No fullscreen, um pouco de scroll interno é aceitável, mas com cuidado.

useMaxHeight e limitação do contêiner

Vamos escrever um wrapper simples que define o máximo de altura para o contêiner raiz:

import { useMaxHeight } from '@/apps-sdk';

export function WidgetContainer(props: { children: React.ReactNode }) {
  const { maxHeight } = useMaxHeight(); // por exemplo, 600

  return (
    <div
      style={{ maxHeight }}
      className="overflow-y-auto p-4 bg-background border border-border rounded-xl"
    >
      {props.children}
    </div>
  );
}

Aqui limitamos honestamente a altura e ativamos o scroll vertical dentro do contêiner, mas com bom senso. Na prática, no inline é melhor evitar muito scroll interno e, em vez de listas enormes, mostrar parte dos dados com um botão “Mostrar mais” ou sugerir o fullscreen.

Altura dinâmica e notifyIntrinsicHeight()

Outro detalhe: seu conteúdo pode mudar de tamanho ao longo do tempo. Por exemplo, primeiro você mostra um spinner “Carregando presentes…”, depois — uma lista com 10 cards, depois o usuário expande/colapsa filtros. Para que o ChatGPT reserve corretamente o espaço do widget e não o corte, é preciso informar ao host o novo valor sempre que a altura mudar. Para isso existe o notifyIntrinsicHeight().

No template isso costuma estar encapsulado em um hook como useAutoResize. Ele pode ser implementado mais ou menos assim:

import { useEffect, useRef } from 'react';
import { useNotifyIntrinsicHeight } from '@/apps-sdk';

export function useAutoResize() {
  const ref = useRef<HTMLDivElement | null>(null);
  const { notifyIntrinsicHeight } = useNotifyIntrinsicHeight();

  useEffect(() => {
    if (!ref.current) return;

    const observer = new ResizeObserver(entries => {
      for (const entry of entries) {
        notifyIntrinsicHeight(entry.contentRect.height);
      }
    });

    observer.observe(ref.current);
    return () => observer.disconnect();
  }, [notifyIntrinsicHeight]);

  return ref;
}

E usamos assim:

export function GiftListInline() {
  const containerRef = useAutoResize();

  return (
    <div ref={containerRef}>
      {/* seu conteúdo */}
    </div>
  );
}

A ideia é simples: quando seu div raiz muda de altura, você chama a API do SDK e o ChatGPT ajusta o contêiner. Esse padrão é explicitamente recomendado por desenvolvedores experientes: um “wrapper auto‑resizer” em volta de todo o conteúdo.

Pequema esquema

Vamos representar isso como um fluxograma:

flowchart TD
    A[O conteúdo do widget mudou] --> B[ResizeObserver detecta a nova altura]
    B --> C["Chamada notifyIntrinsicHeight(newHeight)"]
    C --> D[ChatGPT aumenta/diminui o contêiner]
    D --> E[O usuário vê um scroll adequado sem cortes]

Com tamanhos e altura resolvidos: o widget não deve ultrapassar o espaço reservado nem transformar o uso em um desafio com scroll duplo.

5. Tema (theme), cores e bordas: como fazer o widget parecer “nativo”

Se displayMode e maxHeight determinam quanto espaço temos, o tema (theme) e a paleta definem como esse pedaço de interface aparece dentro do chat.

O ChatGPT oferece pelo menos temas claro e escuro. A plataforma repassa isso ao seu widget por window.openai.theme e/ou em _meta["openai/theme"], e no template React há um hook useOpenAiGlobal("theme") ou algo como useTheme.

A ideia principal: sua UI deve se adaptar ao tema, e não impor o seu próprio.

Obtendo o tema

Exemplo de hook simples:

import { useOpenAiGlobal } from '@/apps-sdk';

export function useThemeMode() {
  const theme = useOpenAiGlobal<'light' | 'dark'>('theme') ?? 'light';
  return { theme };
}

No componente:

export function ThemedCard(props: { children: React.ReactNode }) {
  const { theme } = useThemeMode();

  const className =
    theme === 'dark'
      ? 'bg-slate-900 text-slate-100 border-slate-700'
      : 'bg-white text-slate-900 border-slate-200';

  return (
    <div className={`rounded-xl border p-4 ${className}`}>
      {props.children}
    </div>
  );
}

Em um projeto real, você provavelmente usa Tailwind com darkMode: 'class' e aplica a classe dark no contêiner raiz do widget. Mas isso não muda a essência: o tema vem do Apps SDK, não vive isolado.

Cores, bordas e tipografia

Segundo os guias da OpenAI:

  • use fontes do sistema e tipografia cuidadosa;
  • não sobrescreva as cores do sistema de forma agressiva;
  • o widget deve ser um elemento “nativo” do chat, e não uma landing independente com um gradiente chamativo.

Padrão interessante para o contêiner do GiftGenius:

export function GiftCard(props: { title: string; price: string }) {
  return (
    <div className="rounded-xl border border-border bg-background p-3 flex flex-col gap-2">
      <div className="font-medium text-foreground">{props.title}</div>
      <div className="text-sm text-muted-foreground">{props.price}</div>
      <button className="self-start px-3 py-1 text-sm rounded-full bg-primary text-primary-foreground">
        Selecionar
      </button>
    </div>
  );
}

Aqui supomos que bg-background, border-border, text-foreground, bg-primary etc. são variáveis CSS/classes utilitárias ligadas ao tema do ChatGPT. Essa abordagem também aparece nas recomendações: usar variáveis e classes associadas ao tema, e não codificar cores fixas.

6. Layout e adaptatividade: desktop, mobile, PiP

O terceiro eixo é a largura e o dispositivo. Simplificando bastante, a aparência do widget é determinada pelo modo (displayMode), pela altura disponível (maxHeight) e pela largura disponível (desktop/mobile/PiP).

Nesta seção vamos tratar do terceiro parâmetro. No desktop, o widget inline tem uma largura; no mobile — outra; no PiP há pouquíssimo espaço. O Apps SDK repassa sinais como userAgent, safeArea, às vezes o tamanho do contêiner, que podem ser lidos por useOpenAiGlobal.

Princípios gerais

Alguns princípios importantes a seguir.

Em primeiro lugar, não conte com uma largura fixa. A tela do usuário pode ser estreita (celular) ou larga (desktop grande). Portanto, é melhor construir o layout com flex/grid e auto-fit do que com width: 400px rígido.

Em segundo lugar, evite scroll horizontal. Se sua tabela ou cards não couberem, é melhor ir para fullscreen ou mostrar uma versão reduzida. Também é possível usar um carrossel de slides.

Em terceiro lugar, considere que PiP/modais costumam ser muito estreitos, e não dá para colocar um formulário grande ali — será fisicamente difícil para o usuário interagir.

Esses pontos são destacados na documentação: adaptatividade, safeArea, diferenças desktop vs mobile e o perigo de layouts sobrecarregados.

Layouts diferentes para inline e fullscreen

Voltando ao GiftGenius. A lista de presentes no inline e no fullscreen pode parecer bem diferente. Vamos criar dois componentes.

Inline compacto: no máximo 3 cards, uma coluna no mobile e duas em telas mais largas.

export function GiftListInline() {
  const gifts = useGiftData(); // hook hipotético; buscamos do toolOutput

  return (
    <WidgetContainer>
      <h2 className="text-base font-semibold mb-3">
        Seleção de presentes
      </h2>

      <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
        {gifts.slice(0, 3).map(gift => (
          <GiftCard
            key={gift.id}
            title={gift.title}
            price={`${gift.price} $`}
          />
        ))}
      </div>

      {gifts.length > 3 && (
        <p className="mt-3 text-xs text-muted-foreground">
          Mostradas as 3 primeiras opções. Expanda o widget para ver tudo.
        </p>
      )}
    </WidgetContainer>
  );
}

E a versão fullscreen: grade, filtros, mais cards.

export function GiftListFullscreen() {
  const gifts = useGiftData();
  const [query, setQuery] = useState('');

  const filtered = gifts.filter(g =>
    g.title.toLowerCase().includes(query.toLowerCase()),
  );

  return (
    <div className="h-full flex flex-col gap-4 p-4">
      <header className="flex gap-2 items-center">
        <h1 className="text-lg font-semibold flex-1">
          Presentes para você
        </h1>
        <input
          value={query}
          onChange={e => setQuery(e.target.value)}
          placeholder="Filtrar por nome"
          className="px-2 py-1 text-sm border rounded-md flex-1"
        />
      </header>

      <main className="flex-1 overflow-y-auto">
        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
          {filtered.map(gift => (
            <GiftCard
              key={gift.id}
              title={gift.title}
              price={`${gift.price} $`}
            />
          ))}
        </div>
      </main>
    </div>
  );
}

Aqui permitimos scroll vertical interno no conteúdo fullscreen (overflow-y-auto no main), o que é normal para o modo de tela cheia. A versão inline, como recomendam os guias, continua compacta e facilmente “legível em 2 segundos”.

Esquemático: comportamento por modo

Para fixar, um diagrama simples:

stateDiagram-v2
    [*] --> Inline
    Inline: 3 cards, mínimo de texto
    Inline --> Fullscreen: Clique "Expandir" / "Mostrar tudo"
    Fullscreen: Grade, filtros, muitos dados
    Fullscreen --> Inline: Botão "Fechar" / ação do host
    Fullscreen --> PiP: Operação longa, mostrar progresso
    PiP: Pequeno painel de status
    PiP --> Inline: Operação concluída, mostrar mensagem final

Esse fluxo é muito parecido com os padrões de UX descritos: inline como teaser, fullscreen como ferramenta de trabalho, PiP como indicador de processo.

7. Prática: dois modos do mesmo widget

Hora de consolidar isso em código. Como prática nesta aula, vale fazer duas etapas no aplicativo didático atual.

Etapa 1. Widget inline com card

Amplie o GiftGenius atual para que, no modo inline, o widget:

  • mostre o título “Seleção de presentes”;
  • exiba até três cards de presentes do toolOutput;
  • mostre a dica “Expanda o widget para ver tudo” se houver mais de três presentes;
  • ajuste a altura de forma elegante via useAutoResize e notifyIntrinsicHeight().

Os estilos devem se apoiar no tema: use classes ou variáveis vinculadas ao theme, e não cores fixas.

Etapa 2. Versão fullscreen com formulário

Depois adicione a apresentação em fullscreen, que:

  • mostra título + busca por nome;
  • exibe todos os presentes em uma grade;
  • permite scroll vertical dentro da área principal;
  • fornece um botão “Voltar à conversa” (que chama requestDisplayMode('inline')).

A composição pode ser assim:

export function GiftGeniusWidget() {
  const { displayMode } = useDisplayMode();

  return (
    <>
      <DisplayModeDebug />
      {displayMode === 'fullscreen' ? (
        <GiftListFullscreen />
      ) : (
        <GiftListInline />
      )}
    </>
  );
}

No ChatGPT Dev Mode você poderá alternar o modo manualmente ou solicitar fullscreen programaticamente ao clicar no botão “Mostrar tudo” na versão inline (via useRequestDisplayMode). Esse exercício reforça como o mesmo App pode parecer e se comportar de maneiras diferentes conforme o displayMode.

8. Erros comuns ao gerenciar a aparência do widget

Antes de avançarmos no curso, vamos fixar alguns tropeços comuns relacionados a displayMode, tamanhos, tema e layout. Evitá-los desde o início torna a vida com o Apps SDK muito mais agradável.

Erro nº 1: Ignorar displayMode e tentar “forçar” tudo a parecer fullscreen.
Às vezes os desenvolvedores desenham um layout pesado (quase um SPA separado) que mal cabe no inline. O usuário acaba vendo um Notion em miniatura com barras de rolagem e uma infinidade de elementos. A abordagem correta é projetar apresentações diferentes para modos diferentes e respeitar que inline é um formato compacto, “de uma tela”.

Erro nº 2: Altura fixa enorme e scroll duplo.
Definir height: 800px e esquecer o maxHeight é o caminho para ver seu widget cortado ou gerar scroll interno e externo ao mesmo tempo. O usuário começa a “caçar” a barra certa, o que deteriora o UX. Em vez disso, leia maxHeight, limite via max-height e, quando a altura mudar, informe via notifyIntrinsicHeight().

Erro nº 3: Ignorar o tema e tentar “recolorir tudo com a marca”.
Se você aplicar suas próprias fontes, fundos, gradientes contrastantes e ignorar completamente o tema claro/escuro do ChatGPT, você quebra a unidade visual da plataforma. Os guias dizem claramente: use cores e fontes do sistema, trazendo a marca em toques sutis (botão, ícone, logotipo). Acompanhe o theme via hook e ajuste a paleta.

Erro nº 4: UI complexa demais em PiP/modais.
Tentar colocar um formulário inteiro com muitos campos em uma janela PiP pequena é uma má ideia. Ali cabem apenas casos muito simples: progresso de processo, um ou dois botões, um campo de entrada. Todo o resto é candidato a fullscreen.

Erro nº 5: Fazer layout travado em 800px e não testar no mobile.
Criar layout rígido para 800px e achar que “no celular vai dar um jeito”. Na prática, o cliente mobile do ChatGPT tem outra largura e comportamento, e o PiP é ainda mais estreito. Não esqueça de userAgent/safeArea, use grid/flex sem largura fixa e teste seu widget em layouts estreitos.

Erro nº 6: Trabalhar diretamente com window.openai sem hooks.
Tecnicamente você pode escrever const mode = window.openai.displayMode, mas aí terá que assinar eventos, pensar em updates do React e lidar com bugs se o SDK mudar algo. Os hooks (useDisplayMode, useMaxHeight, useOpenAiGlobal, useRequestDisplayMode) existem justamente para esconder essa rotina e manter o código limpo. Prefira usá-los — sua vida fica bem mais tranquila.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION