Pular para o conteúdo
Fantástico Mundo de Jon
RSS

Um site do zero pelo chat: o HTML sai pronto, o CSS não

Montei um site inteiro copiando e colando do chat. A estrutura saiu quase pronta e a apresentação não — porque HTML tem resposta certa e layout só tem resposta certa para um contexto que o modelo não conhece.

Com pressa? Peça o TL;DR ao Claude — ele lê a página e resume.

A marcação de um site inteiro sai do chat pronta para colar. A folha de estilo não sai — e o motivo não é o modelo ser pior em CSS do que em HTML.

Nas últimas semanas montei um site pequeno de ponta a ponta conversando no navegador: capa, três páginas internas, formulário de contato. Sem agente, sem plugin no editor, sem nada que escrevesse arquivo por mim. Eu pedia, ele respondia, eu selecionava, Ctrl+C, Ctrl+V no VS Code, salvava, recarregava a aba. O método mais burro disponível hoje, e de propósito: existe Cursor, existe Copilot, existe o v0 da Vercel gerando componente com preview — eu queria justamente o cenário sem ferramenta segurando as pontas, porque é assim que a maior parte das pessoas realmente usa isso.

O resultado tem uma assimetria que eu não esperava tão limpa. A estrutura saiu quase pronta. A apresentação não saiu quase nada.

Como foi feito

Período
nov–dez de 2024
Modelos
GPT-4o e Claude 3.5 Sonnet
Método
copiar e colar, sem agente
Editor
VS Code, sem plugin de IA
Stack
HTML + Tailwind 3
Custo
US$ 40/mês (ChatGPT Plus + Claude Pro, US$ 20 cada)

Marcação tem gabarito; layout tem contexto

Essa é a frase inteira do post, e vale gastar um parágrafo nela antes dos exemplos.

Para um dado conteúdo, existe uma marcação certa. Um campo de e-mail é input type="email" com um label associado por for/id. Isso não depende do cliente, da marca, do público nem do meu gosto. É convenção pública, escrita em especificação, repetida em milhões de páginas — exatamente o tipo de coisa que um modelo de linguagem aprende bem, porque a resposta é a mesma independentemente de quem pergunta.

Layout não funciona assim. Qual é o espaço certo entre o título da seção e o primeiro parágrafo? Não existe resposta fora do contexto: depende do tamanho do corpo de texto, da largura da coluna, da densidade do resto da página, de quanto conteúdo vai entrar ali de verdade, de o site ser lido no metrô ou num monitor de 27“. Nenhuma dessas informações está no meu prompt. O modelo não erra a resposta — ele devolve a mediana de todas as respostas plausíveis, que é uma resposta correta para um projeto médio que não é o meu.

É a diferença entre uma pergunta de prova e uma pergunta de projeto. O chat é excelente na primeira e estruturalmente limitado na segunda.

O que sai pronto sai pronto mesmo

Sendo justo com a parte que funciona, porque ela é a maior parte do volume de código.

Pedi o formulário de contato em uma frase — nome, e-mail, telefone, mensagem — e voltou isto:

<form action="/contato" method="post" novalidate>
  <div>
    <label for="nome">Nome</label>
    <input id="nome" name="nome" type="text" autocomplete="name" required />
  </div>
  <div>
    <label for="email">E-mail</label>
    <input id="email" name="email" type="email"
           autocomplete="email" inputmode="email" required />
  </div>
  <div>
    <label for="telefone">Telefone</label>
    <input id="telefone" name="telefone" type="tel"
           autocomplete="tel" inputmode="tel" />
  </div>
  <div>
    <label for="mensagem">Mensagem</label>
    <textarea id="mensagem" name="mensagem" rows="6" required></textarea>
  </div>
  <button type="submit">Enviar mensagem</button>
</form>

Olhe o que está aí sem eu ter pedido: autocomplete correto em cada campo, inputmode para teclado de celular, type="tel" em vez de text, label amarrado por for, required só nos campos que fazem sentido, o botão com type="submit" explícito. Isso é melhor que muito formulário que eu já revisei em produção escrito por gente.

O mesmo vale para a componentização óbvia. Peça um cabeçalho, um rodapé e um cartão de serviço e o corte entre eles vem no lugar que qualquer dev faria. E o texto de preenchimento é decente: em vez de lorem ipsum, vem “Atendimento em até 24 horas úteis” e “Orçamento sem compromisso”, que é ruim como copy final e ótimo como andaime — dá para diagramar em cima e ver o layout com texto do tamanho real, em português, com acento, que é onde o lorem ipsum engana.

Nada disso é pouco. É a parte chata do trabalho, feita em segundos, com uma taxa de acerto alta.

A marcação está certa e a semântica está na média

Agora a primeira rachadura, que é sutil porque não parece rachadura: o HTML válido não é o mesmo que HTML bem estruturado.

O que veio para a capa:

<div class="header">
  <div class="logo">Ateliê Marena</div>
  <div class="menu">
    <div class="menu-item"><a href="/servicos">Serviços</a></div>
    <div class="menu-item"><a href="/contato">Contato</a></div>
  </div>
</div>

<div class="hero">
  <div class="hero-title">Móveis sob medida</div>
  <div class="hero-text">Projeto, marcenaria e instalação.</div>
</div>

<div class="services">
  <div class="service-card">
    <div class="service-title">Projeto 3D</div>
    <div class="service-desc">Você vê antes de aprovar.</div>
  </div>
</div>

Renderiza perfeitamente. Passa no validador. E é uma página sem nenhum ponto de referência para quem não enxerga a tela: nenhum landmark, nenhum cabeçalho de nível, nada que um leitor de tela possa usar para pular direto ao conteúdo ou listar as seções da página.

O que eu tive que escrever à mão:

<header>
  <a href="/" class="logo">Ateliê Marena</a>
  <nav aria-label="Principal">
    <ul>
      <li><a href="/servicos">Serviços</a></li>
      <li><a href="/contato">Contato</a></li>
    </ul>
  </nav>
</header>

<main>
  <section aria-labelledby="titulo-hero">
    <h1 id="titulo-hero">Móveis sob medida</h1>
    <p>Projeto, marcenaria e instalação.</p>
  </section>

  <section aria-labelledby="titulo-servicos">
    <h2 id="titulo-servicos">Serviços</h2>
    <article>
      <h3>Projeto 3D</h3>
      <p>Você vê antes de aprovar.</p>
    </article>
  </section>
</main>

O veredito é o mesmo em todos os casos que eu vi: o modelo escolhe div quando o elemento certo existe, porque div é a resposta que nunca está errada. Não é ignorância — se eu perguntar “qual elemento devo usar para a navegação?”, ele responde nav sem hesitar e explica direitinho o porquê. Ele simplesmente não escolhe o elemento certo quando ninguém cobra, do mesmo jeito que ninguém cobrou na maior parte do HTML da internet em que ele foi treinado.

O contraste reprova sem que ninguém perceba

Este é o meu favorito, porque é o erro que sobrevive a todas as etapas de revisão que uma pessoa comum faz.

Pedi “um botão de destaque, com a cor principal do site”. Voltou:

<button class="bg-indigo-500 text-white px-6 py-3 rounded-lg">
  Solicitar orçamento
</button>

<p class="text-gray-400 mt-2">Resposta em até 24 horas úteis.</p>

Fica bonito. Fica exatamente com a cara do que você imaginou ao ler o parágrafo. E as duas linhas reprovam no critério de contraste do WCAG.

Branco sobre #6366f1 — o indigo-500 do Tailwind 3 — dá 4,47:1. O mínimo para texto normal em AA é 4,5:1. Reprova por três centésimos, uma margem que nenhum olho humano detecta e nenhum navegador avisa. O text-gray-400 é pior e mais comum: #9ca3af sobre branco dá 2,54:1, quase metade do exigido, e é a classe padrão de “texto secundário” em praticamente todo componente que sai do chat.

Par de cores Contraste (n:1) AA texto normal
#ffffff sobre #6366f1 4,47 reprova
#9ca3af sobre #ffffff 2,54 reprova
#6b7280 sobre #ffffff 4,83 passa
#ffffff sobre #4f46e5 6,29 passa
Cálculo pela fórmula de luminância relativa da WCAG 2.x, não medição — dois hexadecimais bastam. As duas primeiras linhas são o que o chat devolveu; as duas últimas são a correção, um degrau na escala do Tailwind. AA pede 4,5:1 para texto normal e 3:1 para texto grande.

A conta cabe em dez linhas e não depende de nenhuma biblioteca:

const canal = (v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));

const luminancia = (hex) => {
  const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
  return 0.2126 * canal(r) + 0.7152 * canal(g) + 0.0722 * canal(b);
};

const contraste = (a, b) => {
  const [x, y] = [luminancia(a), luminancia(b)].sort((m, n) => n - m);
  return (x + 0.05) / (y + 0.05);
};
node contraste.js

#ffffff sobre #6366f1 4.47:1 reprova #9ca3af sobre #ffffff 2.54:1 reprova #6b7280 sobre #ffffff 4.83:1 passa #ffffff sobre #4f46e5 6.29:1 passa

os quatro pares da tabela acima

A correção é ridícula de barata: indigo-500 vira indigo-600 e o branco passa a ter 6,29:1; gray-400 vira gray-500 e o secundário sobe para 4,83:1. Um dígito no hexadecimal separa reprovar de passar. O problema nunca foi a dificuldade da correção — foi ninguém saber que havia algo para corrigir.

A classe que não existe não dá erro

Em algum momento apareceu isto no meu HTML:

<div class="flex-center gap-4 text-md shadow-soft hover:scale-102">

Quatro dessas seis classes não existem no Tailwind 3. gap-4 existe. hover: é prefixo válido. flex-center, text-md, shadow-soft e scale-102 são invenções — utilitários plausíveis, com o nome que eles deveriam ter, que qualquer pessoa juraria fazer parte do framework.

O detalhe cruel: text-md não existe porque a escala do Tailwind é text-sm, text-base, text-lg. scale-102 não existe porque a escala salta de scale-100 para scale-105. flex-center é a fusão óbvia de flex items-center justify-center, que todo mundo já quis que existisse. São exatamente os nomes que um humano chutaria.

O mesmo aconteceu com Bootstrap 5, quando testei a mesma página no outro framework:

<div class="d-flex-center mt-6 text-medium">

mt-6 não existe: a escala de espaçamento do Bootstrap 5 vai de 0 a 5. d-flex-center e text-medium também não. E — este é o ponto — nada disso emite um erro. Não há aviso no console, não há sublinhado vermelho no editor, o build não falha. A classe simplesmente não gera regra nenhuma. O elemento fica sem estilo, você olha, acha que o espaçamento “ficou meio apertado”, e corrige o sintoma no elemento ao lado.

Achei todas as classes inventadas do projeto de uma vez, e nenhuma delas por olhar a tela. Descobri todas de uma vez, olhando o CSS gerado e procurando o que não tinha saída.

Todo hover sem focus apaga a navegação por teclado

Este erro é 100% consistente. Toda vez que pedi um estado de interação, veio só o do mouse:

.botao {
  background: #4f46e5;
  color: #ffffff;
  transition: background 150ms;
}

.botao:hover {
  background: #4338ca;
}

Funciona bem para quem usa mouse e não existe para quem usa teclado. Pior: se em algum momento o CSS levar um outline: none — e leva, porque “tirar aquela borda azul feia do Chrome” é um pedido comum e o modelo atende sem discutir —, a pessoa que navega por Tab perde completamente a noção de onde está na página.

O que o bloco precisa ter:

.botao:hover {
  background: #4338ca;
}

/* o par que nunca vem junto */
.botao:focus-visible {
  outline: 2px solid #b23c17;
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .botao { transition: none; }
}

focus-visible em vez de focus para não acender o anel no clique de mouse; outline-offset para o anel não morrer contra o preenchimento do botão; e a regra de movimento reduzido, que também nunca vem sozinha. Nenhuma dessas três linhas é difícil. Todas as três dependem de alguém lembrar de pedir.

O layout funciona no monitor que o modelo imaginou

O clássico, e o que mais consumiu rodadas de chat. O hero que voltou:

.hero {
  height: 100vh;
  display: grid;
  place-items: center;
}

.servicos {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 24px;
  max-width: 1200px;
}

.depoimento {
  position: absolute;
  top: 420px;
  right: 80px;
  width: 380px;
}

Três armadilhas em doze linhas, todas do mesmo tipo: cada valor foi escolhido para um viewport específico que ninguém declarou.

height: 100vh no celular conta a barra de endereço do navegador, então o conteúdo é cortado ou salta quando a barra recolhe — e se o texto do hero for maior que o previsto, ele vaza para fora do bloco em vez de empurrar a página. repeat(4, 1fr) sem media query vira quatro colunas de 70px num telefone. E o position: absolute com top: 420px é o pior dos três, porque funciona perfeitamente na largura em que o modelo pensou e desmonta em qualquer outra, sem nunca dar sinal de que algo está errado no código.

A versão que resolve os três não é mais complicada, só é escrita sem supor a tela:

.hero {
  min-height: 100svh;      /* svh respeita a barra do navegador móvel */
  display: grid;
  place-items: center;
}

.servicos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1.5rem;
}

min-height em vez de height deixa o conteúdo crescer. auto-fit com minmax reflui sozinho e dispensa breakpoint. E o depoimento vira um item do fluxo, não uma peça flutuando numa coordenada.

Nada disso é conhecimento obscuro, e o modelo conhece tudo: peça explicitamente “grid responsivo sem media query” e ele devolve auto-fit na hora. Ele só não escolhe sozinho, porque repeat(4, 1fr) é o que aparece na maioria dos exemplos de tutorial — e tutorial é escrito para uma tela de captura de tela.

Pedir “deixa bonito” devolve sempre o mesmo site

Aqui está a parte que eu acho realmente útil deste post, e a que mudou meu jeito de trabalhar.

Pedi “deixa mais bonito e moderno” em vários pontos do projeto. Recebi o mesmo site todas as vezes. Não parecido: o mesmo. Gradiente roxo-para-azul no hero, cartão branco com border-radius generoso e sombra difusa, Inter como fonte, ícone arredondado em cima de cada bloco de serviço, uma fileira de três cartões idênticos, e um rodapé escuro. Se você já viu qualquer landing page de produto dos últimos três anos, você já viu exatamente essa página.

E isso não é preguiça do modelo. É o comportamento correto dele.

Um modelo de linguagem devolve a continuação mais provável. “Bonito” e “moderno” são adjetivos sem referente — não apontam para nenhum valor, nenhuma cor, nenhuma medida. Diante de um pedido sem restrição, o único caminho é a média do material de treino, e a média do material de treino de “site bonito e moderno” é literalmente aquilo: o dialeto visual que dominou o Dribbble e os templates de 2021 a 2023. Pedir estética sem restrição é pedir a moda dominante. Você vai receber a moda dominante, com precisão, sempre.

A saída não é escrever um adjetivo melhor. Não existe adjetivo melhor. A saída é parar de mandar adjetivo.

Compare os dois pedidos. Primeiro, o que eu mandava:

Deixa a página mais bonita e moderna, com uma cara mais profissional.

Agora o que eu passei a mandar — mesmo modelo, mesma sessão, mesmo HTML de entrada:

Estilize com estas restrições. Não introduza nenhum valor fora delas.

Cores (só estas):
  fundo       #ffffff
  superfície  #f4f6f4
  texto       #1c211d
  secundário  #5f6b62
  acento      #1f5d3a   -> links, foco e botão primário
  marcação    #b23c17   -> só alerta, no máximo 2% da tela
  régua       #d9e0da   -> 1px, único separador do sistema

Tipografia:
  títulos  Archivo 700, entrelinha 1.06, tracking -0.03em
  corpo    Source Serif 4 400, 19px, entrelinha 1.72, coluna de 68ch
  mono     JetBrains Mono 13px, apenas em código e números

Espaçamento: use só 4, 8, 16, 24, 40, 64 e 104px. Nenhum valor intermediário.
Raio: 0, 2px ou 4px. Nada acima.
Sombra: nenhuma. Profundidade é régua de 1px ou superfície um degrau abaixo.
Estados: todo :hover tem :focus-visible equivalente, anel sólido de 2px.
Contraste: corpo no mínimo 7:1, secundário no mínimo 4,5:1.
Larguras: nenhum container com px fixo. Testar em 320, 768 e 1440.

Se algo do meu HTML não couber nessas regras, me diga em vez de inventar valor.

O segundo devolve um site que é meu. Não porque o modelo ficou mais criativo — ele ficou menos, e é isso que resolve. Com o espaço de resposta fechado, ele deixa de escolher entre um milhão de páginas médias e passa a fazer a única coisa que faz muito bem: aplicar um conjunto de regras a um conjunto de elementos, com consistência sobre-humana. Ele não esquece um espaçamento em vinte arquivos. Eu esqueço.

A troca é clara: estética não é delegável, aplicação de estética é. Alguém precisa decidir a paleta, a escala e as regras — isso é trabalho de projeto, e não sai de um prompt de uma linha. Depois de decidido, o chat é o melhor executor barato que já existiu para isso.

E a última linha do bloco vale por metade dele. “Me diga em vez de inventar valor” troca uma decisão silenciosa — que é cara, porque você só descobre depois — por uma pergunta, que é barata.

Acessibilidade é o buraco que não faz barulho

Junte o que apareceu até aqui e repare no padrão: div no lugar de nav, contraste de 4,47:1, hover sem focus, outline: none, height: 100vh. Todos são falhas de acessibilidade. E nenhuma delas se manifesta.

Erro de sintaxe quebra o build. Erro de lógica quebra o teste. Erro de acessibilidade não faz nada: a página abre, renderiza, fica bonita na captura de tela, o cliente aprova, o site vai ao ar. O feedback só chega por um caminho que a maioria dos projetos nunca percorre — alguém navegando por teclado, alguém usando leitor de tela, alguém com baixa visão no celular sob sol, ou uma auditoria.

Foi o que eu fiz no fim, e é o passo que eu recomendaria a qualquer pessoa que monte um site desse jeito. Uma passada de Lighthouse e uma de axe DevTools, as duas gratuitas e dentro do navegador, acharam em poucos minutos uma lista de problemas que eu não tinha visto em semanas olhando a página. Depois disso, três testes manuais que custam dois minutos cada e pegam o que ferramenta automática não pega:

  • percorrer a página inteira só com Tab e verificar se dá para ver onde o foco está, o tempo todo;
  • dar zoom de 200% e conferir se algum texto sumiu ou se apareceu rolagem horizontal;
  • estreitar a janela até 320px de largura e ver o que quebra.

Nenhum desses três precisa de conhecimento especializado. Precisam só de alguém lembrar de fazer — e é exatamente esse “lembrar” que o chat não faz por você, porque ele responde ao que foi perguntado e ninguém perguntou.

Onde isso vale mesmo assim

Terminei com um site funcionando, e não me arrependo do método. Só sei melhor onde ele se aplica.

Vale muito para protótipo. Quando o objetivo é ver a ideia em tela para decidir se ela sobrevive, o site médio é ótimo — ele é médio justamente por ser familiar, e familiar é o que você quer numa demonstração. Nada ali vai para produção, então dívida de acessibilidade nem chega a nascer.

Vale para landing descartável. Página de um evento que morre em três semanas, formulário de inscrição de uma turma. Custo de manutenção zero, porque não há manutenção.

E vale, com uma ressalva séria, para quem não é do front e precisa de algo que funcione. Um back-end que precisa de um painel interno, alguém montando o site do próprio negócio. Aqui o chat entrega, em uma tarde, algo que essa pessoa não construiria em uma semana. A ressalva é que ela também não tem como enxergar nenhum dos problemas deste post — e por isso a auditoria automática deixa de ser boa prática e vira obrigatória. Duas ferramentas, dez minutos, de graça.

Onde eu não usaria assim: qualquer coisa com marca própria a defender, qualquer coisa que outra pessoa vai manter, qualquer coisa em que acessibilidade seja requisito contratual. Não porque o modelo não dê conta — porque nesses casos o trabalho difícil é decidir as restrições, e essa parte continua sendo minha.

O que ficou, no fim, é uma regra de divisão de trabalho que eu não tinha antes. O modelo é excelente onde existe uma resposta certa e péssimo onde existe apenas uma resposta adequada — e a diferença entre as duas não é dificuldade técnica, é a presença de um contexto que só eu tenho. HTML tem gabarito. CSS tem projeto. Enquanto eu mandar adjetivo, recebo a média da internet; quando mando hexadecimal, escala e regra, recebo o meu site — montado por uma máquina que não erra a vigésima aplicação da mesma regra, que é precisamente onde eu erro.