-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Description
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
- Click "Open AR" → A-Frame scene appears
- Observe:
document.documentElement.classNamenow includes'a-fullscreen' - Click "Close AR" → React unmounts the ARModal component
- Observe:
document.documentElement.classNamestill includes'a-fullscreen' - Try to scroll the page → DOESN'T WORK
- Run
window.scrollTo(0, 500)→ Returnswindow.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 500Expected Behavior
When <a-scene> is removed from the DOM, A-Frame should:
- Remove the
a-fullscreenclass from<html> - Restore the original HTML element styles
- 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:
- The scene is removed via DOM manipulation (not A-Frame's internal destroy)
- The removal happens quickly (React's fast unmount)
- 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
- Is the
a-fullscreenclass necessary for embedded mode? - Should there be a scene attribute to control fullscreen behavior?
- Would you accept a PR for Option 1 above?
- 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