JSX no React: escrever HTML dentro do JavaScript
Como o JSX vira chamada de função, por que é className e não class, como interpolar valores com chaves e o que não pode aparecer dentro do JSX.
Você abre seu primeiro componente React e encontra algo que parece HTML, mas
class virou className, valores ficam entre chaves e duas tags lado a lado
derrubam a compilação. A sensação é de que você precisa decorar uma segunda
linguagem. Não precisa: existe uma única ideia por trás dessas regras.
JSX é uma extensão de sintaxe do JavaScript que deixa você descrever a tela com tags no meio do código. O navegador não entende JSX. Antes de o arquivo chegar a ele, um compilador — um tradutor de código — troca cada tag por uma chamada de função comum. Nesta lição, você vai enxergar essa tradução, corrigir os erros mais frequentes e terminar sabendo ler JSX sem adivinhar.
Os exemplos aqui são o painel da Escola Aurora, uma escola de idiomas: turmas, vagas e matrícula. Se você ainda não criou o projeto, comece por criar o projeto React com Vite — o Vite já vem com o compilador de JSX ligado.
Por que usar essa sintaxe se ela será transformada? Porque uma tela reúne tags, dados e componentes aninhados, e JSX deixa essa estrutura visível enquanto você programa. JSX é açúcar sintático: uma escrita mais conveniente que o compilador transforma antes da execução. Em palavras simples, você escreve uma árvore parecida com a tela; no momento de execução — o runtime —, o JavaScript trabalha com chamadas e objetos.
O que é JSX e por que o navegador não entende?
JSX é a notação que o compilador transforma em chamadas capazes de criar elementos React; o navegador recebe apenas o resultado dessa transformação.
Imagine um roteiro escrito com marcações para atores e cenário. O JSX é o roteiro legível; as tags são as marcações; os valores entre chaves são as informações variáveis da cena; e o compilador é o tradutor que produz instruções para a equipe executar. O navegador não aprende JSX durante esse processo.
O limite da analogia é importante: o compilador não decide o que a tela deve mostrar e não atualiza o DOM. Ele apenas reescreve a sintaxe. Depois, o React usa os objetos produzidos para renderizar a interface. A progressão desta aula segue essa mesma árvore: primeiro uma tag mínima, depois dados de uma turma e, por fim, formulário, condição e estilo de uma interface real.
Este é um componente da Escola Aurora, num arquivo CardTurma.jsx:
function CardTurma() {
return <h2 className="turma">Inglês A2 — terça e quinta</h2>;
}Parece HTML dentro do JavaScript. Não é. Para ver o que sobra, dá para chamar o compilador na mão, com o Babel:
import { transformAsync } from '@babel/core';
import { readFile } from 'node:fs/promises';
const codigo = await readFile('CardTurma.jsx', 'utf8');
const { code } = await transformAsync(codigo, {
presets: [['@babel/preset-react', { runtime: 'automatic', development: false }]],
filename: 'CardTurma.jsx',
});
console.log(code);A tag sumiu. No lugar dela ficou _jsx("h2", { ... }): uma função, um nome de
tag como texto e um objeto de propriedades. E isso não é um jeito do Babel de
fazer as coisas. O esbuild, que é outro compilador, chega ao mesmo formato
sozinho:
npx esbuild CardTurma.jsx --jsx=automaticMuda o apelido da função importada — _jsx no Babel, jsx no esbuild — e o
esbuild ainda escapa tudo que não é ASCII: \xEA é o ê e \u2014 é o
travessão. O formato é o mesmo. E no projeto que você criou com o Vite não é
nenhum dos dois: desde a versão 8 quem traduz o JSX é o Oxc Transformer, que já
vem embutido, e a saída tem exatamente esta forma. Por isso vale entender a forma
uma vez só, em vez de decorar a ferramenta da vez.
E o que essa função devolve? Um objeto JavaScript simples:
import { jsx } from 'react/jsx-runtime';
import util from 'node:util';
const elemento = jsx('h2', { className: 'turma', children: 'Inglês A2 — terça e quinta' });
console.log(util.inspect(elemento, { depth: null }));
console.log(typeof elemento);Guarde essa imagem: <h2> no seu editor é um objeto na memória, com type,
key e props. É por isso que você consegue guardar JSX numa variável, passar
para uma função e devolver de um if. Não é texto de marcação — é valor.
// rodar.mjs — node rodar.mjs arquivo.jsx
import { transformAsync } from '@babel/core';
import { readFile, writeFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
const entrada = process.argv[2];
const { code } = await transformAsync(await readFile(entrada, 'utf8'), {
presets: [['@babel/preset-react', { runtime: 'automatic', development: false }]],
filename: entrada,
});
const saida = entrada.replace(/\.jsx$/, '.gerado.mjs');
await writeFile(saida, code);
await import(pathToFileURL(saida).href);Como colocar JavaScript dentro do JSX?
Use um par de chaves para abrir uma janela de JavaScript dentro da marcação. Nessa janela cabe uma expressão que produz valor: variável, propriedade de objeto, chamada de método ou conta.
import { renderToStaticMarkup } from 'react-dom/server';
const turma = { nome: 'Inglês A2', professora: 'Marina', vagas: 4, mensalidade: 289.9 };
function CardTurma() {
return (
<article className="card">
<h2>{turma.nome}</h2>
<p>Professora: {turma.professora}</p>
<p>Restam {turma.vagas} vagas por R$ {turma.mensalidade.toFixed(2)}</p>
<p>{turma.vagas * 2} lugares na sala</p>
</article>
);
}
console.log(renderToStaticMarkup(<CardTurma />));O detalhe que explica tudo aparece no código compilado. Peguei só a linha do meio e mandei para o Babel:
<p>Restam {turma.vagas} vagas por R$ {turma.mensalidade.toFixed(2)}</p>O texto virou pedaço de um array de filhos, e as chaves viraram as
expressões cruas, sem aspas. Repare também que o nome da função mudou de jsx
para jsxs — o s é de static children, a versão usada quando há mais de um
filho.
Isso é diferente de template literals, que grudam tudo numa string só. Aqui cada pedaço continua sendo um valor separado, e por isso o React consegue trocar só o número de vagas quando ele muda.
Como o que entra nas chaves é um valor, colocar um objeto inteiro estoura:
import { renderToStaticMarkup } from 'react-dom/server';
const turma = { nome: 'Inglês A2', professora: 'Marina' };
function CardTurma() {
return <h2>{turma}</h2>;
}
console.log(renderToStaticMarkup(<CardTurma />));A mensagem entrega o diagnóstico: ela lista as chaves do objeto que você tentou
imprimir. Se aparecerem {nome, professora}, faltou o .nome.
Por que componentes React começam com letra maiúscula?
Porque o compilador usa a inicial para separar uma tag nativa de uma variável que aponta para componente. Essa regra parece capricho de estilo e não é. Veja o que acontece quando o componente começa com minúscula:
import { renderToStaticMarkup } from 'react-dom/server';
function cardTurma() {
return <h2>Inglês A2</h2>;
}
console.log(renderToStaticMarkup(<cardTurma />));O React avisou e mesmo assim entregou uma tag <cardTurma> vazia — sua função
nunca foi chamada. O compilador mostra por quê:
const a = <cardTurma />;
const c = <CardTurma />;Minúscula vira "cardTurma", uma string: nome de tag HTML. Maiúscula vira
CardTurma, uma variável: a sua função. A decisão é tomada na compilação,
antes de o React existir na história.
Por que JSX usa className e htmlFor?
Porque os atributos do JSX viram propriedades de um objeto JavaScript. Como
class e for são palavras reservadas da linguagem, o React usa outros nomes.
Os atributos compostos seguem camelCase: a primeira palavra começa
minúscula e as seguintes começam maiúsculas. Escrevendo do jeito do HTML:
import { renderToStaticMarkup } from 'react-dom/server';
function Matricula() {
return (
<form class="matricula">
<label for="aluno">Nome do aluno</label>
<input id="aluno" maxlength="60" tabindex="1" />
</form>
);
}
console.log(renderToStaticMarkup(<Matricula />));Repare que o React 19 avisa quatro vezes e renderiza mesmo assim. É o pior dos mundos: a tela funciona, o console enche e o problema atravessa a revisão de código. Trate esse aviso como erro.
A versão certa:
import { renderToStaticMarkup } from 'react-dom/server';
function Matricula() {
return (
<form className="matricula">
<label htmlFor="aluno">Nome do aluno</label>
<input id="aluno" maxLength={60} tabIndex={1} />
</form>
);
}
console.log(renderToStaticMarkup(<Matricula />));Os que você mais vai encontrar:
| no HTML | no JSX | por quê |
|---|---|---|
class |
className |
class é palavra reservada do JavaScript |
for |
htmlFor |
for é palavra reservada do JavaScript |
maxlength |
maxLength |
atributo composto vira camelCase |
tabindex |
tabIndex |
atributo composto vira camelCase |
onclick |
onClick |
evento composto vira camelCase |
readonly |
readOnly |
atributo composto vira camelCase |
data-turma |
data-turma |
data- e aria- não mudam |
Por que toda tag precisa ser fechada no JSX?
Porque o compilador precisa saber exatamente onde cada elemento termina. Em HTML
você escreve <img> e o navegador entende. Em JSX, a tag precisa terminar com o
fechamento correspondente.
function Foto() {
return (
<figure>
<img src="/turmas/ingles-a2.jpg" alt="Turma de Inglês A2">
<figcaption>Turma de 2026</figcaption>
</figure>
);
}6 | </figure> 7 | ); > 8 | } | ^ 9 |
O erro está na linha 4 e o compilador aponta a linha 8. Faz sentido do ponto de
vista dele: sem a barra, tudo que vem depois de <img ...> é considerado
conteúdo da tag, e ele só percebe que algo está errado quando o arquivo acaba
sem o fechamento. Quando o erro de JSX apontar para a última linha, procure a
tag aberta mais acima. A correção é <img ... />.
Como retornar dois elementos sem criar uma div?
Use um Fragment, um agrupador que não cria elemento extra no HTML. Ele é
necessário porque um return devolve um valor, enquanto duas tags soltas lado a
lado representam dois valores e nem chegam a compilar:
function Cabecalho() {
return (
<h2>Inglês A2</h2>
<p>Terça e quinta, 19h</p>
);
}2 | return ( 3 | <h2>Inglês A2</h2> > 4 | <p>Terça e quinta, 19h</p> | ^ 5 | ); 6 | }
A solução óbvia é embrulhar numa <div>. O problema é que essa div vai parar
no HTML e atrapalha o layout, principalmente dentro de Flexbox ou Grid. O
Fragment resolve: <> e </>, um embrulho que não vira elemento nenhum.
import { renderToStaticMarkup } from 'react-dom/server';
function Cabecalho() {
return (
<>
<h2>Inglês A2</h2>
<p>Terça e quinta, 19h</p>
</>
);
}
console.log(renderToStaticMarkup(<Cabecalho />));Nenhuma tag extra na saída. Quando você precisa de key — o caso de
listas renderizadas com map — a forma curta não
serve, porque ela não aceita atributo. Aí se escreve <Fragment key={id}>,
importando Fragment do react.
Quais valores aparecem ou somem no JSX?
false, null, undefined e string vazia não deixam texto visível; números,
inclusive zero, aparecem. Para observar a regra inteira, coloquei sete valores
diferentes na mesma lista:
import { renderToStaticMarkup } from 'react-dom/server';
function Linha({ rotulo, valor }) {
return <li>{rotulo}: {valor}</li>;
}
function Diagnostico() {
return (
<ul>
<Linha rotulo="false" valor={false} />
<Linha rotulo="null" valor={null} />
<Linha rotulo="undefined" valor={undefined} />
<Linha rotulo="string vazia" valor={''} />
<Linha rotulo="zero" valor={0} />
<Linha rotulo="NaN" valor={NaN} />
<Linha rotulo="array" valor={['a', 'b']} />
</ul>
);
}
console.log(renderToStaticMarkup(<Diagnostico />));| valor | aparece na tela | consequência prática |
|---|---|---|
false, null, undefined |
nada | é o que permite esconder algo com && |
'' |
nada | string vazia não deixa buraco |
0 |
0 |
número é número: o zero é desenhado |
NaN |
NaN |
conta que deu errado vaza para o usuário |
['a', 'b'] |
ab |
array é achatado e cada item é renderizado |
Essa lista é a origem de um bug clássico. O painel mostra as vagas restantes só quando existem vagas:
import { renderToStaticMarkup } from 'react-dom/server';
function Vagas({ vagas }) {
return (
<div className="vagas">
{vagas && <p>Restam {vagas} vagas</p>}
</div>
);
}
console.log(renderToStaticMarkup(<Vagas vagas={4} />));
console.log(renderToStaticMarkup(<Vagas vagas={0} />));Com a turma cheia, um 0 solto apareceu na tela. O && devolve o valor da
esquerda quando ele é falsy, e 0 é falsy —
mas, ao contrário de false, o React desenha o zero. A correção é transformar a
condição em booleano. Trocando só a linha do &&, no mesmo componente acima:
{vagas > 0 && <p>Restam {vagas} vagas</p>}Como usar style no JSX?
Passe um objeto JavaScript para style, não uma string de CSS. Por isso copiar
o atributo diretamente do HTML não funciona:
import { renderToStaticMarkup } from 'react-dom/server';
function Aviso() {
return <p style="color: crimson; font-weight: bold">Turma quase cheia</p>;
}
console.log(renderToStaticMarkup(<Aviso />));Aqui o React não avisa: ele derruba a renderização. style espera um objeto,
com as propriedades em camelCase:
import { renderToStaticMarkup } from 'react-dom/server';
function Aviso({ vagas }) {
const estilo = {
color: vagas <= 2 ? 'crimson' : 'seagreen',
fontWeight: 'bold',
paddingInline: 8,
borderRadius: 4,
};
return <p style={estilo}>Restam {vagas} vagas</p>;
}
console.log(renderToStaticMarkup(<Aviso vagas={2} />));
console.log(renderToStaticMarkup(<Aviso vagas={9} />));Duas coisas para guardar dessa saída. fontWeight voltou a ser font-weight e
paddingInline virou padding-inline: a conversão é automática. E o número 8
ganhou px sozinho — o React completa a unidade nas propriedades que aceitam
comprimento. Em lineHeight ou flexGrow, que não são comprimento, o número
fica cru.
Quando você escreve style={{ ... }} direto na tag, o par externo de chaves é a
janela do JSX e o interno é o objeto literal. Não são chaves duplas: são duas
coisas diferentes coladas.
Como escrever comentários dentro do JSX?
Abra uma expressão com chaves e coloque dentro dela um comentário de bloco.
Escrever // diretamente no meio da marcação não comenta nada — vira texto na
tela:
import { renderToStaticMarkup } from 'react-dom/server';
function Rodape() {
return (
<footer>
// matrícula aberta até sexta
<p>Escola Aurora</p>
</footer>
);
}
console.log(renderToStaticMarkup(<Rodape />));Faz sentido: dentro de uma tag, o compilador está lendo conteúdo, não código. Para comentar, abra a janela do JavaScript primeiro e use o comentário de bloco:
import { renderToStaticMarkup } from 'react-dom/server';
function Rodape() {
return (
<footer>
{/* matrícula aberta até sexta */}
<p>Escola Aurora</p>
</footer>
);
}
console.log(renderToStaticMarkup(<Rodape />));O comentário de HTML, <!-- -->, não existe em JSX: ele quebra a compilação com
Unexpected token.
Por que if solto entre chaves não compila?
Porque as chaves aceitam uma expressão — algo que produz um valor. if,
for e switch são instruções: mandam fazer, não produzem valor. Por isso
isto nem compila:
function StatusTurma({ vagas }) {
return (
<p>
{if (vagas > 0) { 'Matrícula aberta' } else { 'Turma cheia' }}
</p>
);
}2 | return ( 3 | <p> > 4 | {if (vagas > 0) { ‘Matrícula aberta’ } else { ‘Turma cheia’ }} | ^ 5 | </p> 6 | );
O ternário é a versão-expressão do if, e cabe dentro das chaves:
import { renderToStaticMarkup } from 'react-dom/server';
function StatusTurma({ vagas }) {
return <p>{vagas > 0 ? 'Matrícula aberta' : 'Turma cheia'}</p>;
}
console.log(renderToStaticMarkup(<StatusTurma vagas={4} />));
console.log(renderToStaticMarkup(<StatusTurma vagas={0} />));Se a decisão for maior que um ternário, use if de verdade — só que fora do
return, guardando o JSX numa variável. Como você viu na primeira seção, JSX é
valor: dá para atribuir, comparar e devolver. As formas completas estão em
renderização condicional no React.
O que estudar depois de JSX?
O próximo passo é retirar os dados fixos de dentro do componente e recebê-los de
fora. Agora você sabe ler o que o compilador faz com a sua marcação e reconhecer
os quatro erros que mais aparecem no começo: class em vez de className, tag
sem fechar, dois elementos sem Fragment e o zero do &&. Mas os componentes
daqui ainda têm os dados chumbados dentro deles. Continue em
passar dados com props, para o mesmo CardTurma
servir a todas as turmas da Escola Aurora. Se quiser ver a ordem inteira antes
de continuar, o guia de React mostra onde cada assunto entra.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Preciso instalar o Babel para usar JSX?
Dá para usar React sem escrever JSX?
jsx() ou React.createElement() na mão e o resultado é idêntico. Ninguém faz isso em projeto de tela porque a marcação fica ilegível em três níveis de aninhamento.O arquivo do componente precisa ter extensão .jsx?
.jsx e .tsx; o mesmo código dentro de um .js derruba o build com Unexpected JSX expression. Renomeie o arquivo em vez de mexer na configuração.Como coloco duas classes no className?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, React 19.2.8, @babel/core 8.0.1, esbuild 0.28.2, Vite 8.2.2, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React — Writing Markup with JSX — react.dev
- React — JavaScript in JSX with Curly Braces — react.dev
- React — Writing markup with JSX — react.dev
- Vite — Features: JSX — vite.dev



