Make an element fullscreen in HTML with the Fullscreen API

One method, element.requestFullscreen(), fills the screen with any element. It has to run from a click, it returns a promise, and a frame can refuse it.

To make an element fullscreen in HTML, call element.requestFullscreen() from a click handler. To leave, call document.exitFullscreen() or press Esc. There is no HTML attribute that does it, and the call is refused if it does not come from a user action.

Try the button below. Then read the grey line: it is the live state the API reports.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fullscreen button</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .card {
    max-width: 420px; padding: 18px 20px; border-radius: 12px;
    background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .1);
  }
  .card h2 { margin: 0 0 6px; font-size: 18px; }
  .card p { margin: 0 0 14px; font-size: 14px; color: #4b5563; }
  /* while the card is fullscreen: fill the screen and center the content */
  .card:fullscreen {
    max-width: none; border-radius: 0; box-sizing: border-box;
    display: flex; flex-direction: column; justify-content: center; align-items: center;
    font-size: 1.4em;
  }
  button { font: inherit; font-size: 14px; padding: 9px 14px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
  .log { margin-top: 12px; font: 13px/1.5 ui-monospace, Consolas, monospace; }
  .log div { padding: 6px 8px; border-radius: 6px; background: #eef1f5; margin-top: 6px; overflow-wrap: anywhere; }
  .log .err { background: #fde2da; color: #9a3412; }
</style>
</head>
<body>
<div class="card" id="card">
  <h2>Quarterly numbers</h2>
  <p>Press the button to show this card fullscreen. Esc or the button again exits.</p>
  <button id="btn" type="button">Go fullscreen</button>
</div>
<div class="log">
  <div id="state"></div>
  <div id="msg" hidden></div>
</div>

<script>
  const card = document.getElementById('card');
  const btn = document.getElementById('btn');
  const state = document.getElementById('state');
  const msg = document.getElementById('msg');

  function showState() {
    const el = document.fullscreenElement;  // null when nothing is fullscreen
    state.textContent = 'fullscreenEnabled: ' + document.fullscreenEnabled +
      ' | fullscreenElement: ' + (el ? '#' + el.id : 'null');
    btn.textContent = el ? 'Exit fullscreen' : 'Go fullscreen';
  }

  btn.addEventListener('click', () => {
    msg.hidden = true;
    if (document.fullscreenElement) {
      document.exitFullscreen();
      return;
    }
    // must run inside a click (a user gesture); it returns a promise
    card.requestFullscreen().catch((err) => {
      msg.className = 'err';
      msg.hidden = false;
      msg.textContent = 'Refused: ' + err.name + ': ' + err.message +
        (document.fullscreenEnabled ? '' :
          '. This page is inside an embedded frame without the fullscreen permission. Opened as its own page, the same code works.');
    });
  });

  // fires on enter and on exit, including when the user presses Esc
  document.addEventListener('fullscreenchange', showState);
  showState();
</script>
</body>
</html>
A fullscreen button with a state readout. If the browser refuses, the error appears in red.

Inside this article, the example runs in an embedded frame without the fullscreen permission. So here the button shows the refusal instead: fullscreenEnabled is false and the promise rejects with a TypeError.

Opened as its own page, the same code fills the screen. The sections below explain both outcomes.

The whole API in four pieces

Piece What it does
el.requestFullscreen() Asks to show el fullscreen. Returns a promise
document.exitFullscreen() Leaves fullscreen. Also returns a promise
document.fullscreenElement The element that is fullscreen now, or null
fullscreenchange / fullscreenerror Events on the document for enter/exit, and for refusals

A toggle button needs only these four:

btn.addEventListener('click', () => {
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    card.requestFullscreen().catch((err) => console.log(err.message));
  }
});

// runs on enter and on every exit, including Esc
document.addEventListener('fullscreenchange', () => {
  btn.textContent = document.fullscreenElement ? 'Exit fullscreen' : 'Go fullscreen';
});

To show the whole page instead of one element, call it on the root: document.documentElement.requestFullscreen().

It must come from a click

The browser only goes fullscreen in response to a user action, such as a click or a key press. A call on page load or from a timer is refused. The promise rejects, fullscreenerror fires, and nothing changes on screen.

A click starts the request. The browser then allows or refuses it, and Esc always ends it.
A click starts the request. The browser then allows or refuses it, and Esc always ends it.

Because it is a promise, always attach a .catch(). Without one, a refusal is silent apart from a console message. With one, you can tell the user, or switch to a fallback as the finished example does.

Exiting, and knowing when it happened

Users leave fullscreen with Esc more often than with your button. Esc does not run your click handler, so a label that you only update inside the handler ends up wrong.

Listen for fullscreenchange on document instead. It fires on every enter and every exit, whatever caused it. Inside the listener, document.fullscreenElement tells you which one: an element means you just entered, null means you just left.

Styling with :fullscreen and ::backdrop

While an element is fullscreen, the :fullscreen selector matches it. The browser stretches it to fill the screen and paints a ::backdrop behind it, black by default.

The common surprise: an element with no background of its own is transparent, so the black backdrop shows through. Dark text then nearly disappears. Give the element a background in its :fullscreen rule.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>:fullscreen and ::backdrop styling</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .bar { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; margin-bottom: 12px; font-size: 14px; }
  button { font: inherit; padding: 8px 12px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
  .note { font-size: 12px; color: #6b7280; margin: 0 0 10px; }

  .chart { padding: 16px; border-radius: 12px; border: 1px solid #dfe3e8; max-width: 420px; }
  .chart.with-bg { background: #fff; }
  .chart h3 { margin: 0 0 12px; font-size: 16px; }
  .bars { display: flex; align-items: flex-end; gap: 10px; height: 110px; }
  .bars span { flex: 1; border-radius: 6px 6px 0 0; background: #60a5fa; }

  /* the real fullscreen rules, and the same rules for the simulation class */
  .chart:fullscreen, .chart.sim {
    max-width: none; border: 0; border-radius: 0; padding: 6vmin; box-sizing: border-box;
  }
  .chart:fullscreen .bars, .chart.sim .bars { height: 60vh; }
  .chart:fullscreen h3, .chart.sim h3 { font-size: 5vmin; }

  /* ::backdrop sits behind the fullscreen element; the default is black */
  .chart::backdrop { background: #0b1220; }

  /* simulation only: pin the card over the frame, with a fake backdrop */
  .chart.sim { position: fixed; inset: 0; z-index: 2; }
  .fake-backdrop { position: fixed; inset: 0; background: #0b1220; z-index: 1; }
  .sim-label { position: fixed; right: 10px; bottom: 10px; z-index: 3; font-size: 12px; padding: 6px 9px; border-radius: 6px; background: #f59e0b; color: #111; }
</style>
</head>
<body>
<div class="bar">
  <button id="real" type="button">Real fullscreen</button>
  <button id="sim" type="button">Simulate fullscreen</button>
  <label><input id="bg" type="checkbox" checked> card has a background</label>
</div>
<p class="note" id="note">"Simulate" applies the :fullscreen rules with a class, so you can see them even where real fullscreen is refused.</p>

<div class="chart with-bg" id="chart">
  <h3>Sign-ups per week</h3>
  <div class="bars">
    <span style="height:40%"></span><span style="height:65%"></span><span style="height:50%"></span><span style="height:85%"></span><span style="height:100%"></span>
  </div>
</div>
<div class="fake-backdrop" id="fake" hidden></div>
<div class="sim-label" id="label" hidden>SIMULATION: click anywhere to close</div>

<script>
  const chart = document.getElementById('chart');
  const fake = document.getElementById('fake');
  const label = document.getElementById('label');
  const note = document.getElementById('note');

  document.getElementById('bg').addEventListener('change', (e) => {
    chart.classList.toggle('with-bg', e.target.checked);
  });

  document.getElementById('real').addEventListener('click', () => {
    chart.requestFullscreen().catch((err) => {
      note.textContent = 'Real fullscreen was refused here (' + err.message + '). Try "Simulate fullscreen".';
    });
  });

  function setSim(on) {
    chart.classList.toggle('sim', on);
    fake.hidden = !on;
    label.hidden = !on;
  }
  document.getElementById('sim').addEventListener('click', (e) => {
    e.stopPropagation();
    setSim(true);
  });
  document.addEventListener('click', (e) => {
    if (chart.classList.contains('sim') && e.target.id !== 'sim') setSim(false);
  });
</script>
</body>
</html>
Untick the background box, then simulate. The orange label marks the simulation.

Real fullscreen is refused in this frame, so the demo has a Simulate fullscreen button. It adds a class that carries the same rules, pins the card over the frame, and draws a fake backdrop. It shows what would change; it is not the API.

.chart:fullscreen {
  background: #fff;   /* otherwise the backdrop shows through */
  padding: 6vmin;
}
.chart:fullscreen .bars { height: 60vh; }
.chart::backdrop { background: #0b1220; }

Inside an iframe: allow="fullscreen"

A page in an <iframe> from another origin, or in a sandboxed iframe, can only go fullscreen if the page that embeds it allows it. The parent grants it with the allow attribute.

Without it, document.fullscreenEnabled is false inside the frame and every request is refused.

The permission is set on the iframe tag in the parent page, not inside the framed page.
The permission is set on the iframe tag in the parent page, not inside the framed page.
<iframe src="https://other.example/viewer.html" allow="fullscreen"></iframe>

The older allowfullscreen attribute does the same job. A frame from the same origin as the parent, without a sandbox, is allowed by default.

This applies to sandboxed frames too. None of the sandbox flags grants fullscreen, so a sandboxed iframe also needs allow="fullscreen". The sandbox attribute covers what the flags do, and the iframe guide covers embedding in general.

If you cannot change the parent page, the framed page cannot fix it. Check document.fullscreenEnabled first and offer a fallback.

A fallback that fills the window with CSS

When fullscreen is not available, fill the window instead. position: fixed with inset: 0 makes the element cover the whole viewport of the page or frame it is in. It is plain CSS, so no permission or gesture is needed.

Fullscreen takes the whole monitor. The CSS version fills the tab or frame it lives in.
Fullscreen takes the whole monitor. The CSS version fills the tab or frame it lives in.
.viewer.expanded { position: fixed; inset: 0; z-index: 10; }

The trade-off: the browser bars stay visible, and Esc does nothing unless you listen for it. It is a good second choice, and on phones where the API is limited it may be the only one.

Finished example: a slide viewer

This viewer uses real fullscreen where it is allowed and falls back to filling the window where it is not. In this article, the fallback is what you see. The arrow keys move between slides in both modes.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Slide viewer with fullscreen</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #eceef1; }
  .viewer {
    position: relative; height: 330px; border-radius: 12px; overflow: hidden;
    background: #111827; color: #fff; outline: none;
  }
  .slide {
    position: absolute; inset: 0 0 52px; display: none;
    place-items: center; text-align: center; padding: 20px;
    font-size: clamp(20px, 6vmin, 64px); font-weight: 700;
  }
  .slide.on { display: grid; }
  .s1 { background: linear-gradient(135deg, #1d4ed8, #7c3aed); }
  .s2 { background: linear-gradient(135deg, #047857, #0ea5e9); }
  .s3 { background: linear-gradient(135deg, #b45309, #db2777); }
  .controls {
    position: absolute; left: 0; right: 0; bottom: 0; height: 52px;
    display: flex; align-items: center; gap: 8px; padding: 0 10px; background: #111827;
  }
  .controls button { font: inherit; font-size: 14px; padding: 7px 11px; border: 0; border-radius: 7px; background: #374151; color: #fff; cursor: pointer; }
  .controls .grow { flex: 1; font-size: 13px; color: #9ca3af; }

  /* real fullscreen and the fallback share one look */
  .viewer:fullscreen { border-radius: 0; }
  .viewer.expanded { position: fixed; inset: 0; height: auto; border-radius: 0; z-index: 10; }
</style>
</head>
<body>
<div class="viewer" id="viewer" tabindex="0">
  <div class="slide s1 on">Q3 review</div>
  <div class="slide s2">Sign-ups +18%</div>
  <div class="slide s3">Next: launch in October</div>
  <div class="controls">
    <button id="prev" type="button" aria-label="Previous slide">&larr;</button>
    <button id="next" type="button" aria-label="Next slide">&rarr;</button>
    <span class="grow" id="info"></span>
    <button id="full" type="button">Fullscreen</button>
  </div>
</div>

<script>
  const viewer = document.getElementById('viewer');
  const slides = viewer.querySelectorAll('.slide');
  const info = document.getElementById('info');
  const full = document.getElementById('full');
  let i = 0;

  function show(n) {
    slides[i].classList.remove('on');
    i = (n + slides.length) % slides.length;
    slides[i].classList.add('on');
    render();
  }

  function isFull() {
    return !!(document.fullscreenElement || document.webkitFullscreenElement);
  }

  function render() {
    const mode = isFull() ? 'fullscreen' : viewer.classList.contains('expanded') ? 'filling the window' : 'normal';
    info.textContent = (i + 1) + ' / ' + slides.length + ' · ' + mode;
    full.textContent = mode === 'normal' ? 'Fullscreen' : 'Exit';
  }

  // Fallback when fullscreen is not allowed: fill the window with CSS
  function expand(on) {
    viewer.classList.toggle('expanded', on);
    render();
  }

  function enter() {
    const req = viewer.requestFullscreen || viewer.webkitRequestFullscreen;  // older Safari uses the prefix
    const allowed = document.fullscreenEnabled || document.webkitFullscreenEnabled;
    if (!req || !allowed) return expand(true);
    Promise.resolve(req.call(viewer)).catch(() => expand(true));
  }

  function exit() {
    if (isFull()) (document.exitFullscreen || document.webkitExitFullscreen).call(document);
    else expand(false);
  }

  full.addEventListener('click', () => (isFull() || viewer.classList.contains('expanded') ? exit() : enter()));
  document.getElementById('prev').addEventListener('click', () => show(i - 1));
  document.getElementById('next').addEventListener('click', () => show(i + 1));

  document.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowRight') show(i + 1);
    if (e.key === 'ArrowLeft') show(i - 1);
    if (e.key === 'Escape') expand(false);  // real fullscreen handles Esc itself
  });

  document.addEventListener('fullscreenchange', render);
  document.addEventListener('webkitfullscreenchange', render);
  viewer.addEventListener('pointerdown', () => viewer.focus());
  render();
</script>
</body>
</html>
Click a slide, then use the arrow keys. Fullscreen falls back to filling the window when it is refused.
  • Feature check: if document.fullscreenEnabled is false, or the method is missing, it goes straight to the CSS fallback.
  • Older Safari: it also tries webkitRequestFullscreen, webkitExitFullscreen and webkitfullscreenchange, the prefixed names older Safari versions use.
  • Refusals: if the promise rejects anyway, the .catch() switches to the fallback.
  • Esc: real fullscreen handles Esc itself. The fallback listens for it on keydown.

For a single picture rather than slides, zooming an image on click is often enough and needs no permission.

On iPhone

iPhone Safari has had limits on the Fullscreen API. For a long time, only video elements could go fullscreen there, and requestFullscreen was not available on other elements.

Do not assume either way: test on a real iPhone, and keep the fill-the-window fallback so the button always does something.

When it does not work

What you see Cause Fix
Nothing happens on page load No user gesture Call it inside a click or key handler
TypeError, and fullscreenEnabled is false Inside a cross-origin or sandboxed iframe without allow="fullscreen" Add allow="fullscreen" to the iframe tag, or use the CSS fallback
requestFullscreen is not a function in older Safari Only the prefixed name exists Also try webkitRequestFullscreen
Text vanishes on a black screen The element has no background, so ::backdrop shows through Set a background in the :fullscreen rule
:fullscreen styles never apply F11 browser fullscreen, not the API Enter with requestFullscreen()
Button still says "Exit" after Esc State only updated in the click handler Listen for fullscreenchange
Nothing happens on iPhone Safari limits fullscreen on iPhone Feature-check and fall back to position: fixed

A fullscreen viewer is easier to show than to describe. A screenshot cannot be clicked through, and an .html attachment may open as plain code on a phone.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can flip through the slides themselves.

Build it with the fallback, as above, so the button does something whether or not the page is allowed to go fullscreen. If you change the code later, the same link shows the new version.

Questions people ask

Is there an HTML attribute that makes an element fullscreen?

No. Fullscreen needs JavaScript: call element.requestFullscreen() from a click or key press. The allow="fullscreen" attribute on an iframe does not make anything fullscreen by itself. It only permits the page inside the frame to ask.

Why does requestFullscreen() do nothing when the page loads?

The browser only grants fullscreen in response to a user action such as a click or a key press. Called on load or from a timer, the promise rejects with a TypeError and the fullscreenerror event fires.

How do I exit fullscreen with JavaScript?

Call document.exitFullscreen(). It is a method of the document, not of the element. The user can also press Esc, so listen for the fullscreenchange event to notice every exit, not only the ones your button starts.

Why does :fullscreen not apply when I press F11?

F11 is the browser's own fullscreen mode. It hides the browser bars, but no element enters fullscreen, so document.fullscreenElement stays null and :fullscreen does not match. Only requestFullscreen() triggers the API.

Does the Fullscreen API work on iPhone?

iPhone Safari has had limits here: for a long time only video elements could go fullscreen. Check that the method exists and that document.fullscreenEnabled is true before relying on it, test on a real iPhone, and keep a fill-the-window fallback.

Keep reading