Skip to content

a-fullscreen Class Not Removed on Scene Unmount #5786

@markstrefford

Description

@markstrefford

A-Frame Bug Report: a-fullscreen Class Not Removed on Scene Unmount

Summary

When an <a-scene> is unmounted from the DOM (e.g., React component unmount), A-Frame leaves the a-fullscreen class on the <html> element, causing the entire page to remain locked with position: fixed and preventing scroll functionality.

Environment

  • A-Frame Version: 1.4.2
  • Browser: Chrome 131 (also reproduces in Safari)
  • OS: macOS 14.7.1
  • Framework: React 18.3.1
  • Reproduction: Consistent (100% reproducible)

The Problem

What A-Frame Does

When <a-scene> initializes, it adds the a-fullscreen class to the <html> element, which applies:

html.a-fullscreen {
  position: fixed;
  height: 100vh;
  overflow: hidden;
}

This is intended to make the VR experience immersive and full-screen.

What Goes Wrong

When the <a-scene> element is removed from the DOM (via React unmount, or any DOM manipulation), A-Frame does not remove the a-fullscreen class from <html>.

Result: The entire page remains locked at viewport height with position: fixed, making it impossible to scroll even after the scene is gone.

Steps to Reproduce

Minimal React Example

// ARModal.tsx
import React, { useEffect } from 'react';

export const ARModal = ({ onClose }: { onClose: () => void }) => {
  useEffect(() => {
    // Load A-Frame (or assume already loaded)
    const script = document.createElement('script');
    script.src = 'https://aframe.io/releases/1.4.2/aframe.min.js';
    document.head.appendChild(script);

    return () => {
      // Cleanup: remove scene
      document.querySelectorAll('a-scene').forEach(el => el.remove());
    };
  }, []);

  return (
    <div style={{ position: 'fixed', inset: 0, background: '#000', zIndex: 9999 }}>
      <button onClick={onClose} style={{ position: 'absolute', top: 20, right: 20 }}>
        Close AR
      </button>

      <a-scene embedded>
        <a-box position="0 1 -3" color="red"></a-box>
        <a-sky color="#222"></a-sky>
        <a-camera></a-camera>
      </a-scene>
    </div>
  );
};

// App.tsx
import React, { useState } from 'react';
import { ARModal } from './ARModal';

export default function App() {
  const [showAR, setShowAR] = useState(false);

  return (
    <div>
      <div style={{ height: '200vh', padding: 50 }}>
        <h1>Scrollable Page</h1>
        <button onClick={() => setShowAR(true)}>Open AR</button>
        <p>This page should be scrollable...</p>
        <div style={{ marginTop: '100vh' }}>
          <p>...but after closing AR, you can't scroll here!</p>
        </div>
      </div>

      {showAR && <ARModal onClose={() => setShowAR(false)} />}
    </div>
  );
}

Steps

  1. Click "Open AR" → A-Frame scene appears
  2. Observe: document.documentElement.className now includes 'a-fullscreen'
  3. Click "Close AR" → React unmounts the ARModal component
  4. Observe: document.documentElement.className still includes 'a-fullscreen'
  5. Try to scroll the page → DOESN'T WORK
  6. Run window.scrollTo(0, 500) → Returns window.scrollY === 0 (can't scroll programmatically either)

Verification

// After closing AR:
console.log(document.documentElement.className); // Contains 'a-fullscreen'
console.log(getComputedStyle(document.documentElement).position); // 'fixed'
console.log(getComputedStyle(document.documentElement).height); // '100vh' or viewport height in px

// Manual fix proves the issue:
document.documentElement.classList.remove('a-fullscreen');
window.scrollTo(0, 500);
console.log(window.scrollY); // Now works! Shows 500

Expected Behavior

When <a-scene> is removed from the DOM, A-Frame should:

  1. Remove the a-fullscreen class from <html>
  2. Restore the original HTML element styles
  3. Allow the page to scroll normally again

Actual Behavior

The a-fullscreen class persists indefinitely, keeping the page locked even though the scene is gone.

Impact

Severity: High

This affects any use case where A-Frame is toggled on/off within a larger application:

  • Modal-based AR/VR experiences
  • Single-page applications (SPAs) with A-Frame views
  • Progressive web apps with optional AR features
  • Any React/Vue/Angular integration where components mount/unmount

User Experience Impact

  • Users cannot scroll the page after closing AR
  • Page appears broken
  • Requires full page reload to restore scroll
  • Not obvious what's wrong (no console errors)

Root Cause Analysis

Looking at A-Frame source code, the a-fullscreen class is added but the cleanup logic doesn't reliably remove it when:

  1. The scene is removed via DOM manipulation (not A-Frame's internal destroy)
  2. The removal happens quickly (React's fast unmount)
  3. The scene hasn't fully initialized before removal

The class is likely managed in the scene lifecycle, but the pause() or remove() handlers aren't triggered properly when React removes the element.

Proposed Solutions

Option 1: Add Cleanup in A-Frame Core (Recommended)

Add a disconnectedCallback or remove handler that explicitly removes a-fullscreen:

// In a-scene component
remove: function () {
  // Existing cleanup...
  
  // Ensure fullscreen class is removed
  document.documentElement.classList.remove('a-fullscreen');
},

pause: function () {
  // Existing pause logic...
  
  // Remove fullscreen when paused/hidden
  document.documentElement.classList.remove('a-fullscreen');
}

Option 2: Use MutationObserver (More Robust)

Watch for scene removal and clean up automatically:

// In scene initialization
const observer = new MutationObserver(() => {
  if (!document.querySelector('a-scene')) {
    document.documentElement.classList.remove('a-fullscreen');
    observer.disconnect();
  }
});

observer.observe(document.body, { childList: true, subtree: true });

Option 3: Make it Optional (Backward Compatible)

Add a scene attribute to control this behavior:

<a-scene auto-cleanup-fullscreen="true">

Default to true for new users, allow false for backward compatibility.

Current Workaround

Users must manually clean up in their application code:

// When unmounting/closing AR:
useEffect(() => {
  return () => {
    document.documentElement.classList.remove('a-fullscreen');
    document.querySelectorAll('a-scene').forEach(el => el.remove());
  };
}, []);

But this is:

  • Non-obvious to developers
  • Easy to forget
  • Shouldn't be the user's responsibility
  • Still causes issues if unmount is interrupted

Additional Context

Why This Wasn't Caught Earlier

Most A-Frame examples/demos:

  • Run full-page (no scroll needed)
  • Never unmount the scene (static pages)
  • Reload the entire page when done

Modern SPA patterns expose this issue because scenes are frequently mounted/unmounted.

Similar Issues

This is similar to modal libraries that lock body scroll - they all have explicit cleanup:

  • react-modal: Removes body classes on unmount
  • radix-ui: Restores scroll on dialog close
  • Material-UI: Cleanup in useEffect return

A-Frame should follow the same pattern.

Testing Suggestions

Unit Test

describe('a-scene cleanup', () => {
  it('should remove a-fullscreen class when scene is removed', () => {
    const scene = document.createElement('a-scene');
    document.body.appendChild(scene);
    
    // Wait for initialization
    scene.addEventListener('loaded', () => {
      expect(document.documentElement.classList.contains('a-fullscreen')).toBe(true);
      
      // Remove scene
      scene.remove();
      
      // Give time for cleanup
      setTimeout(() => {
        expect(document.documentElement.classList.contains('a-fullscreen')).toBe(false);
      }, 100);
    });
  });
});

Integration Test

Test with React/Vue/Angular component lifecycle to ensure cleanup works with framework unmounts.

Related Issues

  • [Link to any existing GitHub issues if found]
  • This may be related to VR mode exit issues
  • Possibly affects embedded mode differently

Reproduction Repository

[If submitting to GitHub, include a link to minimal repro repo]

Questions for Maintainers

  1. Is the a-fullscreen class necessary for embedded mode?
  2. Should there be a scene attribute to control fullscreen behavior?
  3. Would you accept a PR for Option 1 above?
  4. Are there other cleanup tasks that might be missed on unmount?

Priority

High - This is a critical bug for SPA integrations and breaks core functionality (scrolling) after AR closes.


Contact: [Your GitHub username]
Willing to Submit PR: Yes
Tested A-Frame Versions: 1.4.2 (latest), likely affects earlier versions too

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions