Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori12 min de leitura

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.

html
<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:

js
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);
Escola de Música Clave Agenda 1 null Sábado lotado 1

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:

html
<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:

Refused to display 'https://www.youtube.com/' in a frame because it set 'X-Frame-Options' to 'sameorigin'.

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}:

html
<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:

embed-normal.html 20 req 1152 KB 10 hosts 5 cookies .youtube.com embed-nocookie.html 17 req 1150 KB 8 hosts 0 cookies

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:

html
<!-- 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ó:

Refused to display 'https://www.google.com/' in a frame because it set 'X-Frame-Options' to 'sameorigin'.

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:

html
<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>
frame-title [serious] Frames must have an accessible name <iframe src="https://www.youtube.com/embed/-4jGj8GZ52M" width="560" height="315"></iframe> -> Fix any of the following: Element has no title attribute aria-label attribute does not exist or is empty aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty Element's default semantics were not overridden with role="none" or role="presentation" axe-core 4.13.0

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:

html
<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>
js
for (const id of ['imagem', 'video']) {
  console.log(id.padEnd(8) + getComputedStyle(document.getElementById(id)).aspectRatio);
}
imagem auto 560 / 315 video auto

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:

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

html
<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>
sem loading 19 requisicoes 1150 KB 8 dominios loading="lazy" 2 requisicoes 1 KB 1 dominio

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:

js
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:

html
<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>
== sem allowfullscreen fullscreen bloqueado camera bloqueado microphone bloqueado encrypted-media bloqueado autoplay bloqueado == com allowfullscreen fullscreen liberado camera bloqueado microphone bloqueado encrypted-media liberado autoplay liberado

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:

html
<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:

html
<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>
== sem sandbox -> a pagina de cima terminou em: /parceiro.html | Promoção do parceiro == sandbox (vazio) Blocked script execution in 'http://localhost:8412/widget.html' because the document's frame is sandboxed and the 'allow-scripts' permission is not set. -> a pagina de cima terminou em: /sandbox-teste.html | Escola de Música Clave == sandbox="allow-scripts" Unsafe attempt to initiate navigation for frame with origin 'http://localhost:8412' from frame with URL 'http://localhost:8412/widget.html'. The frame attempting navigation of the top-level window is sandboxed, but the flag of 'allow-top-navigation' or 'allow-top-navigation-by-user-activation' is not set. o widget tentou trocar a página de cima e levou: SecurityError -> a pagina de cima terminou em: /sandbox-teste2.html | Escola de Música Clave

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:

bash
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/watch?v=-4jGj8GZ52M x-frame-options: SAMEORIGIN

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:

Refused to display 'http://localhost:8412/' in a frame because it set 'X-Frame-Options' to 'deny'. Framing 'https://www.uol.com.br/' violates the following Content Security Policy directive: "frame-ancestors 'self' *.intranet *.uolinc.com". The request has been blocked.

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:

js
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:

HTTP/1.1 200 OK X-Powered-By: Express X-Frame-Options: DENY Content-Type: text/html; charset=utf-8 Content-Length: 34 ETag: W/"22-a5ZqcDVL+Oj7DS4YaWusmxVKa5E" Date: Sat, 22 Aug 2026 20:53:14 GMT Connection: keep-alive Keep-Alive: timeout=5

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.

Ver todos os vídeos do canal
  • html
  • iframe
  • youtube
  • sandbox
  • incorporar

Perguntas frequentes

iframe é ruim para SEO?
O conteúdo de dentro do iframe não conta como conteúdo da sua página — ele pertence à outra URL. Isso não é penalidade, é contabilidade. O problema aparece quando o texto que deveria ser indexado mora só lá dentro: aí a sua página fica vazia aos olhos do buscador.
O botão voltar do navegador funciona direito com iframe?
Nem sempre. Navegar dentro do iframe grava uma entrada no histórico da aba, então o botão voltar desfaz o passo de dentro do iframe em vez de sair da sua página. Quem só assiste ao vídeo não percebe; quem navegou cinco telas dentro de um iframe fica preso apertando voltar.
Dá para saber, pelo JavaScript da minha página, o que o usuário fez dentro do iframe?
Se o conteúdo for de outra origem, não. O navegador bloqueia a leitura do contentDocument e a comunicação precisa ser combinada dos dois lados, com window.postMessage. Só o conteúdo da mesma origem pode ser lido direto.
Qual a diferença entre src e srcdoc?
Com src o navegador busca uma URL. Com srcdoc você escreve o HTML do documento filho ali mesmo, no atributo. O srcdoc é útil para preview de código e, combinado com sandbox, para exibir HTML que veio do usuário.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — O elemento <iframe> — developer.mozilla.org
  2. MDN — X-Frame-Options — developer.mozilla.org
  3. MDN — CSP: frame-ancestors — developer.mozilla.org
  4. YouTube — Incorporar vídeos e playlists — support.google.com

Continue por aqui