Safari on visionOS now exposes a JavaScript immersive API that lets websites transport users into full 3D environments using the HTML model element, mirroring the Fullscreen API pattern with requestImmersive(). Developers can build inline 3D previews and seamless transitions into spatial environments directly from a webpage.
⢠Web developers can create fully immersive spatial experiences (virtual theaters, escape rooms, marketing environments) that launch from a standard webpage in Safari on Apple Vision Pro ā no app required.
⢠The API follows the existing Fullscreen API pattern, so any developer familiar with requestFullscreen() can adopt requestImmersive() with minimal friction.
⢠Environments built for visionOS apps (USDZ assets, RealityKit scenes) can be repurposed for web marketing experiences, dramatically extending ROI on existing spatial content.
Demonstrates how a SwiftUI WKWebView host can load a locally served webpage that uses the HTML model element and requestImmersive() to show a 3D theater environment, and how the native layer observes the immersive state change message posted back from JavaScript.
import SwiftUI
import WebKit
// MARK: - Message names bridged from JavaScript
struct JSMessage {
static let immersiveChanged = "immersiveChanged"
static let immersiveError = "immersiveError"
}
// MARK: - SwiftUI wrapper
struct ImmersiveWebView: UIViewRepresentable {
@Binding var isImmersive: Bool
func makeCoordinator() -> Coordinator {
Coordinator(isImmersive: $isImmersive)
}
func makeUIView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
// Register native message handlers so JS can notify Swift
config.userContentController.add(
context.coordinator,
name: JSMessage.immersiveChanged
)
config.userContentController.add(
context.coordinator,
name: JSMessage.immersiveError
)
// Inject JS that wires up the immersive API and posts messages back
let script = WKUserScript(
source: theaterScript,
injectionTime: .atDocumentEnd,
forMainFrameOnly: true
)
config.userContentController.addUserScript(script)
let webView = WKWebView(frame: .zero, configuration: config)
webView.scrollView.isScrollEnabled = true
// Load the locally served theater-seat page
if let url = URL(string: "https://localhost:8080/theater") {
webView.load(URLRequest(url: url))
}
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
// MARK: - Injected JavaScript (bridging requestImmersive ā WKScriptMessage)
private var theaterScript: String {
"""
(function() {
// Check availability before showing the immersive button
const supported = document.xrEnvironment?.isImmersiveSupported ?? false;
const btn = document.getElementById('enter-btn');
if (btn) btn.style.display = supported ? 'block' : 'none';
const model = document.getElementById('theater-model');
if (!model) return;
// Listen for immersive state changes and relay to Swift
model.addEventListener('immersivechange', () => {
const immersive = document.xrEnvironment?.isImmersive ?? false;
window.webkit.messageHandlers.immersiveChanged.postMessage({ isImmersive: immersive });
});
model.addEventListener('immersiveerror', (e) => {
window.webkit.messageHandlers.immersiveError.postMessage({ error: e.message });
});
if (btn) {
btn.addEventListener('click', async () => {
try {
await model.requestImmersive();
} catch(err) {
window.webkit.messageHandlers.immersiveError.postMessage({ error: err.message });
}
});
}
})();
"""
}
// MARK: - Coordinator (WKScriptMessageHandler)
final class Coordinator: NSObject, WKScriptMessageHandler {
@Binding var isImmersive: Bool
init(isImmersive: Binding<Bool>) {
_isImmersive = isImmersive
}
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard let body = message.body as? [String: Any] else { return }
switch message.name {
case JSMessage.immersiveChanged:
if let value = body["isImmersive"] as? Bool {
DispatchQueue.main.async { self.isImmersive = value }
}
case JSMessage.immersiveError:
let err = body["error"] as? String ?? "Unknown error"
print("[ImmersiveWeb] Error: \(err)")
default:
break
}
}
}
}
// MARK: - Root view
struct ContentView: View {
@State private var isImmersive = false
var body: some View {
ZStack(alignment: .bottom) {
ImmersiveWebView(isImmersive: $isImmersive)
.ignoresSafeArea()
if isImmersive {
Text("You are inside the theater ā use the Digital Crown to exit")
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
.padding(.bottom, 40)
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.animation(.easeInOut, value: isImmersive)
}
}
Liquid Glass is Apple's new material and visual design language introduced in iOS 27, bringing translucent, refractive glass-like surfaces to system and custom UI elements. It replaces the frosted vibrancy aesthetic with a more dynamic, depth-aware material that responds to content beneath it.
PaperKit is Apple's full-featured canvas framework ā previously internal-only ā now publicly available in iOS/macOS/visionOS 27. It powers the drawing and markup experience in Notes, Preview, and Freeform, giving developers access to a complete pencil, shapes, images, and text canvas with a rich data model.
Xcode 27 introduces a fully customizable toolbar and theme system, untitled scratch projects, coding agent integration directly in the editor, and the new Device Hub for evaluating apps across simulators and physical devices side-by-side.
In-depth guide
RealityKit & USDKit in iOS 27 ā⢠requestImmersive() must be called in direct response to a user gesture ā calling it programmatically without a user interaction will be rejected. ⢠Inline and immersive presentations use different reference frames: inline origin is the center of the CSS layer, immersive origin is at the user's feet. Entity transforms must be recalculated on every immersive state change. ⢠Setting display:none on the model element defers asset download until the immersive request fires ā useful for heavy assets but means the first immersive transition will be slower. ⢠The Digital Crown always dismisses the immersive environment; apps must listen to immersive state change events and update UI accordingly rather than assuming an exit only happens via an in-page button. ⢠USDZ video docking and light-spill features require custom RealityKit annotations baked into the asset (demonstrated with a Blender plugin); these are not yet a web standard.
Requires Apple Vision Pro hardware; the immersive API is only available in Safari on visionOS. Inline model element previews work on macOS and iOS, but requestImmersive() is gated by document.xrEnvironment?.isImmersiveSupported (or equivalent availability check) and will only be active on visionOS.
iOS 27 introduces new SwiftUI drag and drop APIs including reorderable, reorderContainer, dragContainer, and configuration modifiers that enable reordering within and across collections, multi-item drag, and fine-grained control over how data is transferred during drag and drop operations.