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 |
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
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
Tabe 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.