Skip to content

Iframe embedding best practices for games

Many platforms (Itch.io, Newgrounds, Cinevva, your own site) embed games in iframes. This tutorial covers how to make your game work well when embedded.

1) Basic embeddable setup

Your game should work without requiring full page control:

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    * { margin: 0; padding: 0; }
    html, body { width: 100%; height: 100%; overflow: hidden; }
    canvas { display: block; width: 100%; height: 100%; }
  </style>
</head>
<body>
  <canvas id="game"></canvas>
  <script src="game.js"></script>
</body>
</html>

2) Detecting iframe context

js
const isEmbedded = window.self !== window.top

if (isEmbedded) {
  // Adjust behavior for embedded context
  hideExternalLinks()
  adjustUIForSmallSize()
}

function hideExternalLinks() {
  document.querySelectorAll('a[target="_blank"]').forEach(link => {
    link.style.display = 'none'
  })
}

3) Allow necessary iframe permissions

The parent page controls what your iframe can do. Common permissions:

html
<iframe 
  src="https://game.example.com"
  allow="fullscreen; autoplay; gamepad; pointer-lock"
  sandbox="allow-scripts allow-same-origin allow-pointer-lock allow-popups"
></iframe>

Your game should handle missing permissions gracefully:

js
// Check if fullscreen is available
const canFullscreen = document.fullscreenEnabled || document.webkitFullscreenEnabled

if (!canFullscreen) {
  document.getElementById('fullscreen-btn').style.display = 'none'
}

4) Fullscreen from iframe

Fullscreen requires the allow="fullscreen" attribute:

js
async function requestFullscreen() {
  const elem = document.documentElement
  
  try {
    if (elem.requestFullscreen) {
      await elem.requestFullscreen()
    } else if (elem.webkitRequestFullscreen) {
      await elem.webkitRequestFullscreen()
    }
  } catch (err) {
    // Fullscreen not allowed - show message
    showMessage('Fullscreen not available when embedded')
  }
}

5) Communicating with parent page

Use postMessage for safe cross-origin communication:

In your game:

js
// Send message to parent
function notifyParent(type, data) {
  if (window.parent !== window) {
    window.parent.postMessage({ type, data, source: 'game' }, '*')
  }
}

// Receive messages from parent
window.addEventListener('message', (event) => {
  // Validate origin if needed
  // if (event.origin !== 'https://trusted-host.com') return
  
  const { type, data } = event.data
  
  switch (type) {
    case 'pause':
      pauseGame()
      break
    case 'resume':
      resumeGame()
      break
    case 'setVolume':
      setVolume(data.volume)
      break
  }
})

// Notify when game is ready
window.addEventListener('load', () => {
  notifyParent('ready', { width: 800, height: 600 })
})

// Notify on game events
function onGameOver(score) {
  notifyParent('gameover', { score })
}

In parent page:

js
const iframe = document.getElementById('game-iframe')

iframe.addEventListener('load', () => {
  // Listen for messages from game
  window.addEventListener('message', (event) => {
    if (event.source !== iframe.contentWindow) return
    if (event.data.source !== 'game') return
    
    const { type, data } = event.data
    
    if (type === 'ready') {
      console.log('Game ready:', data)
    }
    if (type === 'gameover') {
      showScoreModal(data.score)
    }
  })
})

// Send commands to game
function pauseGame() {
  iframe.contentWindow.postMessage({ type: 'pause' }, '*')
}

6) Handling focus

Iframes can lose focus, breaking keyboard input:

js
// Auto-focus canvas when clicked
canvas.addEventListener('click', () => {
  canvas.focus()
})

// Make canvas focusable
canvas.tabIndex = 1

// Handle focus loss
window.addEventListener('blur', () => {
  // Reset held keys
  input.left = input.right = input.up = input.down = false
  
  if (isEmbedded) {
    // Optionally pause
    // pauseGame()
  }
})

// Request focus from parent
function requestFocus() {
  notifyParent('requestFocus', {})
}

7) Responsive embedded size

Handle various embed dimensions:

js
function handleResize() {
  const width = window.innerWidth
  const height = window.innerHeight
  
  // Adjust UI based on size
  if (width < 400 || height < 300) {
    enableCompactUI()
  } else {
    enableFullUI()
  }
  
  // Scale game appropriately
  resizeCanvas(width, height)
}

window.addEventListener('resize', handleResize)
handleResize()

8) Loading indicator for embeds

Show something immediately:

js
// Inline in HTML for instant display
const loadingHTML = `
  <div id="loading" style="
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #1a1a2e;
    color: #fff;
    font-family: sans-serif;
  ">
    <div>
      <div class="spinner"></div>
      <p>Loading...</p>
    </div>
  </div>
`

// Remove when game is ready
function hideLoading() {
  const loading = document.getElementById('loading')
  if (loading) {
    loading.style.opacity = '0'
    loading.style.transition = 'opacity 0.3s'
    setTimeout(() => loading.remove(), 300)
  }
}

9) Platform-specific SDKs

Some platforms have their own APIs:

Itch.io:

js
// No SDK required, but can use postMessage for achievements
window.parent.postMessage({ type: 'itch-achievement', data: { id: 'first-win' }}, '*')

Newgrounds:

js
// Include Newgrounds.io SDK
const ngio = new Newgrounds.io.core('APP_ID', 'AES_KEY')
ngio.callComponent('Medal.unlock', { id: 12345 })

Cinevva:

js
// Signal game is ready
window.parent.postMessage({ type: 'cinevva:ready' }, '*')

// Signal playable state
window.parent.postMessage({ type: 'cinevva:playable' }, '*')

10) Security considerations

js
// Validate message origins when sensitive
window.addEventListener('message', (event) => {
  const trustedOrigins = [
    'https://itch.io',
    'https://cinevva.com',
    'https://yoursite.com'
  ]
  
  if (!trustedOrigins.includes(event.origin)) {
    return // Ignore untrusted messages
  }
  
  // Process message...
})

// Don't expose sensitive operations via postMessage
// Only allow whitelisted commands
const allowedCommands = ['pause', 'resume', 'setVolume', 'mute']

window.addEventListener('message', (event) => {
  const { type } = event.data
  if (!allowedCommands.includes(type)) return
  
  // Handle command...
})

Testing checklist

  • Local testing: Use a simple HTTP server, not file://
  • Cross-origin: Test with actual iframe embedding
  • Permissions: Test with restricted sandbox
  • Focus: Test keyboard after clicking outside iframe
  • Resize: Test at various embed sizes
  • Mobile: Test touch in embedded context
html
<!-- Test embed page -->
<!DOCTYPE html>
<html>
<body style="background: #333; padding: 20px;">
  <h1 style="color: #fff;">Embed Test</h1>
  <iframe 
    src="http://localhost:8000" 
    width="800" 
    height="600"
    allow="fullscreen; autoplay; gamepad"
  ></iframe>
</body>
</html>