iframe no HTML: incorporar YouTube, mapa e outra página
Como usar a tag iframe com segurança: title, loading e sandbox, e por que alguns sites simplesmente se recusam a abrir dentro de um.
A tag <iframe> abre um buraco na sua página e encaixa outro site inteiro nele.
O navegador carrega ali dentro um documento completo — com HTML, CSS e
JavaScript próprios — que só divide com você o retângulo na tela. É assim que
entram o vídeo do YouTube, o mapa do Google e o widget de pagamento.
Todos os exemplos desta lição são do site da Escola de Música Clave: uma página
de contato com o vídeo de apresentação, o mapa da unidade de Pinheiros e um
widget de depoimentos feito por terceiros. Cada saída colada aqui saiu de uma
execução real: as do navegador no Chrome 151 headless, dirigido pelo protocolo do
DevTools; as de cabeçalho HTTP, no curl.
O nome técnico desse limite é contexto de navegação aninhado. Em palavras simples, o iframe reserva um retângulo na sua interface, mas o documento que vive ali tem endereço, DOM e execução próprios.
O apartamento vizinho dentro da sua moldura
Imagine uma janela que permite ver o apartamento ao lado. Ele aparece dentro
da sua parede, mas continua com porta, moradores e regras próprias. O iframe é
a moldura; a página incorporada mantém seu documento. A política de mesma
origem e o atributo sandbox determinam quais interações atravessam essa
separação.
No primeiro exemplo, anote a URL da página principal e a do iframe, depois
inspecione os dois documentos no DevTools. Tente localizar em qual árvore está
cada h1. Essa verificação evita tratar o conteúdo incorporado como se fosse
mais uma div do documento pai.
Comece pelo caso mais simples possível: em vez de apontar para uma URL, escreva
o HTML do documento filho direto no atributo srcdoc.
<h1>Escola de Música Clave</h1>
<iframe title="Agenda de aulas"
srcdoc="<h1>Agenda</h1><p id='aviso'>Sábado lotado</p>"></iframe>Agora abra o console e tente enxergar, de fora, o que está lá dentro:
const agenda = document.querySelector('iframe').contentDocument;
console.log(document.querySelector('h1').textContent);
console.log(agenda.querySelector('h1').textContent);
console.log(document.querySelectorAll('h1').length);
console.log(document.getElementById('aviso'));
console.log(agenda.getElementById('aviso').textContent);
console.log(window.frames.length);Repare na terceira linha. A página tem dois h1 na tela, e o
querySelectorAll da página de fora enxerga um. O #aviso existe, está
visível, e document.getElementById('aviso') devolve null.
É a diferença entre um iframe e qualquer outra tag que você já usou nas
tags e atributos do HTML: o iframe não
insere elementos na sua árvore, ele instala uma segunda árvore ao lado. Para
atravessar, você precisa passar pelo contentDocument — e isso só funciona
quando os dois documentos são da mesma origem, como aqui.
O vídeo de apresentação: /embed no lugar de /watch
O erro mais comum é copiar a URL da barra de endereço do YouTube e colar no
src:
<iframe title="Apresentação da escola"
src="https://www.youtube.com/watch?v=-4jGj8GZ52M"
width="560" height="315"></iframe>O retângulo fica cinza e o console explica:
A página /watch é a interface do YouTube, e ela se recusa a ser exibida dentro
do site de outra pessoa. Quem foi feita para isso é /embed/{id}:
<iframe title="Apresentação da Escola de Música Clave"
src="https://www.youtube.com/embed/-4jGj8GZ52M"
width="560" height="315"
allow="accelerometer; clipboard-write; encrypted-media; picture-in-picture"
allowfullscreen></iframe>Você troca -4jGj8GZ52M pelo id do seu vídeo — é o pedaço depois de v= na
URL. O botão “Compartilhar → Incorporar”, no próprio YouTube, entrega esse HTML
pronto.
Existe uma variante do domínio, youtube-nocookie.com, que o YouTube chama de
“modo de privacidade aprimorada”. Carreguei a mesma página com as duas versões e
contei o que cada uma deixou no navegador antes de qualquer clique no play:
O peso não muda: pouco mais de 1,1 MB nas duas, porque o player é o mesmo player. O que muda são os cookies — cinco contra zero. Se a sua página tem aviso de cookies, essa troca de domínio muda o que você precisa declarar nele.
O mapa da unidade, sem chave de API
O Google Maps repete a mesma armadilha. A URL que você copia da barra de endereço não abre em iframe; a que abre é a de incorporação:
<!-- não funciona: é a interface do Maps -->
<iframe title="Mapa" src="https://www.google.com/maps/place/Rua+dos+Pinheiros"
width="600" height="450"></iframe>
<!-- funciona: modo de incorporação -->
<iframe title="Mapa da unidade Pinheiros"
src="https://www.google.com/maps?q=Rua+dos+Pinheiros&output=embed"
width="600" height="450"></iframe>Com os dois na mesma página, o console reclama uma vez só:
O primeiro retângulo fica com o ícone de página quebrada; o segundo desenha o
mapa da Rua dos Pinheiros, com o alfinete no lugar. O parâmetro output=embed
é o que muda tudo, e ele não exige chave de API nem conta de faturamento.
Vale saber de onde ele vem: o Google nunca documentou esse parâmetro. O caminho
oficial é a Maps Embed API, que pede chave e conta de faturamento. Para marcar o
endereço de uma unidade numa página de contato, o output=embed resolve; para um
produto que depende do mapa, prefira o caminho que tem contrato por trás.
title, width e height: o trio que o auditor cobra
title não é enfeite. Para quem navega com leitor de tela, o iframe é anunciado
como uma região da página — e sem title essa região não tem nome. Rodei o
axe-core numa página com dois embeds
iguais, um com title e outro sem:
<iframe src="https://www.youtube.com/embed/-4jGj8GZ52M" width="560" height="315"></iframe>
<iframe title="Apresentação da Escola de Música Clave"
src="https://www.youtube.com/embed/-4jGj8GZ52M" width="560" height="315"></iframe>Uma violação, e ela aponta exatamente para o iframe sem title. O outro passou.
O título descreve o conteúdo, não o elemento: “Apresentação da Escola de
Música Clave” serve, “iframe” e “vídeo” não. A mesma lógica do alt que você
viu em acessibilidade em HTML.
width e height continuam sendo atributos do HTML, e não CSS: eles reservam o
retângulo antes de o primeiro byte do outro site chegar, e a página para de pular
quando o conteúdo entra. Só que aqui o iframe não se comporta como a
imagem, e essa diferença derruba muito layout
responsivo. Coloquei os dois lado a lado, com os mesmos atributos:
<img id="imagem" width="560" height="315" src="/fachada.jpg" alt="Fachada da escola">
<iframe id="video" title="Apresentação da escola" width="560" height="315"
src="https://www.youtube.com/embed/-4jGj8GZ52M"></iframe>for (const id of ['imagem', 'video']) {
console.log(id.padEnd(8) + getComputedStyle(document.getElementById(id)).aspectRatio);
}A imagem converte os atributos em proporção — é isso que auto 560 / 315
significa. O iframe devolve auto puro: para ele os atributos viram largura e
altura fixas, e proporção nenhuma. O resultado prático aparece no CSS. Com
width: 100% sozinho, o vídeo estica na horizontal e fica com os 315 px de altura
do atributo; somando height: auto, ele desaba para 150 px, que é a altura padrão
de um iframe. A proporção você declara na mão:
iframe {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}Com essas três linhas, o mesmo iframe numa janela de 1280 px mede 1264 × 711 — 16 / 9 exato, em qualquer largura.
loading="lazy": o megabyte que só desce se o leitor descer
Um embed do YouTube não é um retângulo: é uma aplicação. Montei a página da
escola com seis mil pixels de conteúdo antes do vídeo — bem longe da primeira
tela, numa janela de 1280 × 800 — e contei as requisições disparadas até o
load, com e sem loading="lazy" no iframe.
<h1>Escola de Música Clave</h1>
<div style="height:6000px">Aulas de violão, piano e canto desde 2008.</div>
<h2>Apresentação da escola</h2>
<iframe title="Apresentação da Escola de Música Clave"
src="https://www.youtube.com/embed/-4jGj8GZ52M"
width="560" height="315" loading="lazy"
allow="accelerometer; clipboard-write; encrypted-media; picture-in-picture"
allowfullscreen></iframe>Em quatro execuções a linha de cima repetiu as mesmas 19 requisições, com o total entre 1149 e 1151 KB; a de baixo nunca saiu de duas requisições e 1 KB — a própria página e o favicon. Nada do YouTube foi baixado, nenhum dos sete domínios de terceiros foi contatado, e o vídeo só começa a carregar quando o leitor chega perto dele.
allow e allowfullscreen: o que o conteúdo de fora pode pedir
Um documento de outra origem chega ao iframe sem permissão nenhuma de hardware.
Quem devolve permissão é o atributo allow. Para provar, coloquei dentro do
iframe um script que pergunta ao próprio navegador o que ele pode fazer:
const pp = document.featurePolicy;
for (const recurso of ['fullscreen', 'camera', 'microphone', 'encrypted-media', 'autoplay']) {
console.log(recurso.padEnd(16) + (pp.allowsFeature(recurso) ? 'liberado' : 'bloqueado'));
}Depois carreguei esse mesmo documento em dois iframes de outra origem — um pelado, outro com permissões declaradas:
<iframe title="Player da aula" src="http://127.0.0.1:8412/tela-cheia.html"></iframe>
<iframe title="Player da aula" src="http://127.0.0.1:8412/tela-cheia.html"
allow="encrypted-media; autoplay" allowfullscreen></iframe>Tudo bloqueado por padrão, e liberado item a item — exatamente o que você
escreveu no allow, nada além. camera e microphone continuam fechados nos
dois casos, porque ninguém pediu. É por isso que o HTML que o YouTube entrega
vem com aquela lista comprida: cada palavra ali é uma porta que o player precisa
para funcionar direito.
sandbox: o widget de terceiro dentro de uma cela
Agora o caso perigoso. A escola contratou um widget de depoimentos e colou o iframe na página. O widget faz isto:
<h1>Depoimentos dos alunos</h1>
<script>
try {
top.location.href = '/parceiro.html';
} catch (e) {
console.log('o widget tentou trocar a página de cima e levou: ' + e.name);
}
</script>Carreguei a página da escola três vezes, mudando só o atributo sandbox do
iframe, e anotei onde a página de cima terminou:
<iframe title="Depoimentos" src="/widget.html"></iframe>
<iframe title="Depoimentos" src="/widget.html" sandbox></iframe>
<iframe title="Depoimentos" src="/widget.html" sandbox="allow-scripts"></iframe>Leia a primeira linha com calma: sem sandbox, o widget levou o visitante
embora do site da escola. Ninguém clicou em nada. Um iframe comum pode navegar
a janela inteira, e essa é uma das formas clássicas de sequestro de tráfego.
O atributo sandbox sem valor nenhum é o mais restrito que existe: nem script
roda. Cada palavra que você adiciona devolve uma permissão. Com
allow-scripts, o script do widget voltou a rodar — e a tentativa de trocar a
página de cima virou um SecurityError, porque allow-top-navigation não foi
concedido.
“Refused to display”: quando o outro site decide que não
A recusa não é bug seu, e nenhum atributo do seu HTML resolve. Ela vem de um cabeçalho na resposta HTTP do outro site. Dá para ler sem abrir o navegador:
curl -sI -A "Mozilla/5.0" "https://www.youtube.com/watch?v=-4jGj8GZ52M" \
| grep -iE "^(x-frame-options|content-security-policy: *frame-ancestors)"Rodando isso em quatro endereços:
https://www.youtube.com/embed/-4jGj8GZ52M
https://www.bb.com.br X-Frame-Options: SAMEORIGIN
https://www.uol.com.br content-security-policy: frame-ancestors ‘self’ *.intranet *.uolinc.com;
Aqui está a explicação inteira em quatro blocos. /watch manda
X-Frame-Options: SAMEORIGIN e por isso é recusado; /embed não manda
cabeçalho nenhum e por isso funciona. O Banco do Brasil usa o mesmo
SAMEORIGIN — nenhum banco vai deixar você reproduzir a tela de login dele
dentro de uma página sua, e o motivo tem nome: clickjacking. O UOL usa a forma
moderna, frame-ancestors do Content-Security-Policy, que é mais expressiva:
ele lista quais domínios podem embutir a página.
Numa página de teste com o portal do aluno da Clave — que responde DENY, como
você vê logo abaixo — e o UOL lado a lado, o Chrome escreve exatamente isto:
Repare que, no caso do X-Frame-Options, o Chrome mostra só a origem da
página recusada, sem o caminho. Se você tem três iframes na página, a mensagem
não diz qual deles falhou — a aba Network do DevTools diz.
Do outro lado do balcão, é assim que você protege as suas páginas. O portal do
aluno da Clave é servido por um Express, e a única linha que importa é o
res.set:
app.get('/portal', (req, res) => {
res.set('X-Frame-Options', 'DENY');
res.type('html').send('<h1>Portal do Aluno — Clave</h1>');
});Batendo nessa rota com curl -sI, o cabeçalho aparece na terceira linha:
Qualquer página com login, formulário de pagamento ou dado pessoal deveria sair
com esse cabeçalho, ou com Content-Security-Policy: frame-ancestors 'none',
que é o equivalente atual.
Onde o iframe é a ferramenta errada
O iframe resolve um problema específico: exibir conteúdo de outra origem que você não controla. Fora disso, ele costuma ser a escolha cara.
| situação | por que o iframe atrapalha | o que usar |
|---|---|---|
| menu e rodapé repetidos em toda página | o buscador indexa cada iframe como URL separada e o histórico do navegador embaralha | include do servidor, componente, ou build estático |
| formulário da sua própria aplicação | o CSS do site não atravessa, e a validação fica em dois documentos | <form> na própria página |
| vídeo hospedado por você | o player nativo é mais leve e você controla legenda e poster | a tag <video> |
| desenho, gráfico ou ícone | um documento inteiro para desenhar uma forma | SVG inline ou canvas |
Site inteiro montado em frames — cabeçalho num, menu noutro, conteúdo num
terceiro — foi moda nos anos 1990, primeiro com o <frameset> e depois com
iframe, e deixou herança ruim: link que não dá para compartilhar, botão “voltar”
imprevisível, e conteúdo que o buscador atribui à URL errada. Se o conteúdo é
seu, ele pertence à mesma página.
O que vem depois
Se o vídeo é hospedado por você em vez de vir do YouTube, o caminho é outro: veja vídeo e áudio em HTML, onde o player nativo substitui o embed inteiro. Para desenho e gráfico, o guia completo de HTML mostra onde entram SVG e canvas — e a trilha de HTML traz a ordem de estudo até aqui.
O exercício desta lição é curto: abra a página de contato do seu projeto, some
title e loading="lazy" em cada iframe que estiver abaixo da primeira tela, e
compare a aba Network antes e depois. A ordem de grandeza vai ser a mesma que
apareceu aqui: dezenas de requisições e mais de um megabyte que deixam de sair.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
iframe é ruim para SEO?
O botão voltar do navegador funciona direito com iframe?
Dá para saber, pelo JavaScript da minha página, o que o usuário fez dentro do iframe?
Qual a diferença entre src e srcdoc?
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 Chrome 151.0.7922.170 (headless), Node 24.16.0 e curl 8.7.1 no macOS, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — O elemento <iframe> — developer.mozilla.org
- MDN — X-Frame-Options — developer.mozilla.org
- MDN — CSP: frame-ancestors — developer.mozilla.org
- YouTube — Incorporar vídeos e playlists — support.google.com



