FlutterFlow has no native VR headset support — Flutter does not connect to Oculus, Vision Pro, or SteamVR. What you can build is compelling mobile VR: 360° panorama photos with gyroscope-driven navigation, 3D model viewers, and Google Cardboard split-screen mode. These work well for virtual tours, 360° product showcases, and real estate previews.
Flutter VR: what's real and what's not
Before building, it's important to be clear about Flutter's VR capabilities. Flutter does not support VR headset runtimes. There is no Oculus SDK, no Apple Vision Pro visionOS support, no SteamVR integration. Flutter renders to a 2D canvas — it's a mobile UI framework, not a 3D game engine. What Flutter does support is mobile VR experiences: 360° equirectangular photos displayed on a sphere with gyroscope and touch navigation (the same experience as Google Street View), 3D model viewers using WebGL-backed packages, and Google Cardboard's simple left-right split-screen format. For most business use cases — virtual property tours, 360° product photography, immersive training content — this is entirely sufficient and deployable to iOS and Android without any headset.
Prerequisites
- FlutterFlow Pro plan (Custom Widgets required for panorama and 3D packages)
- A 360° equirectangular image (available free on sites like polyhaven.com for testing)
- Basic understanding of FlutterFlow Custom Widgets
- Physical iOS or Android device for testing — gyroscope features don't work in web browser
Step-by-step guide
Add the panorama_viewer package to your FlutterFlow project
Add the panorama_viewer package to your FlutterFlow project
In FlutterFlow, go to Settings → Pubspec Dependencies → Add Dependency. Search for 'panorama_viewer' and add the latest version (1.x+). Also add 'sensors_plus' to enable gyroscope-driven navigation. Click Save. These packages enable 360° photo viewing with touch drag and automatic tilting when the user moves their phone. Next, upload your 360° equirectangular image to Firebase Storage or use a public URL. A standard 360° photo is a 2:1 aspect ratio image — if you photograph with a standard camera it won't work. Use a 360° camera or download test images from polyhaven.com.
Expected result: panorama_viewer and sensors_plus packages added in Settings. A 360° test image URL ready to use.
Create a PanoramaViewer Custom Widget
Create a PanoramaViewer Custom Widget
Go to Custom Code → Custom Widgets → Add Widget. Name it 'PanoramaViewer360'. This widget fills a given area and renders a 360° equirectangular image on a sphere that the user can navigate by dragging or by moving their phone. Set parameters: imageUrl (String), enableSensors (boolean, default true), and initialHorizontalAngle (double, default 0.0). Paste the code below. After saving, drag the widget onto any page and set the imageUrl parameter to your 360° image URL.
1// Custom Widget: PanoramaViewer3602// Package: panorama_viewer ^1.0.03import 'package:flutter/material.dart';4import 'package:panorama_viewer/panorama_viewer.dart';56class PanoramaViewer360 extends StatelessWidget {7 const PanoramaViewer360({8 super.key,9 required this.imageUrl,10 this.enableSensors = true,11 this.initialLongitude = 0.0,12 this.initialLatitude = 0.0,13 this.sensitivity = 1.0,14 this.width = double.infinity,15 this.height = 300.0,16 });1718 final String imageUrl;19 final bool enableSensors;20 final double initialLongitude;21 final double initialLatitude;22 final double sensitivity;23 final double width;24 final double height;2526 @override27 Widget build(BuildContext context) {28 return SizedBox(29 width: width,30 height: height,31 child: PanoramaViewer(32 sensitivity: sensitivity,33 sensorControl: enableSensors34 ? SensorControl.absoluteOrientation35 : SensorControl.none,36 longitude: initialLongitude,37 latitude: initialLatitude,38 child: Image.network(39 imageUrl,40 fit: BoxFit.cover,41 loadingBuilder: (context, child, loadingProgress) {42 if (loadingProgress == null) return child;43 return Center(44 child: CircularProgressIndicator(45 value: loadingProgress.expectedTotalBytes != null46 ? loadingProgress.cumulativeBytesLoaded /47 loadingProgress.expectedTotalBytes!48 : null,49 ),50 );51 },52 ),53 ),54 );55 }56}Expected result: A 360° interactive panorama renders on the page. Touch-dragging pans the view. On a physical device, tilting the phone also pans the view.
Add a 3D model viewer with flutter_3d_controller
Add a 3D model viewer with flutter_3d_controller
For displaying interactive 3D product models or virtual objects, add the flutter_3d_controller package. This package renders .glb (binary glTF) and .obj files with WebGL acceleration. Add it in Settings → Pubspec Dependencies. Create a Custom Widget named 'Model3DViewer'. The widget renders a 3D scene where the user can rotate and zoom the model by dragging and pinching. Upload your .glb file to Firebase Storage. Good sources for free 3D models: sketchfab.com (download as .glb) and Google's Poly archive.
1// Custom Widget: Model3DViewer2// Package: flutter_3d_controller ^1.3.03import 'package:flutter/material.dart';4import 'package:flutter_3d_controller/flutter_3d_controller.dart';56class Model3DViewer extends StatefulWidget {7 const Model3DViewer({8 super.key,9 required this.modelUrl,10 this.backgroundColor,11 this.autoPlay = true,12 this.width = double.infinity,13 this.height = 300.0,14 });1516 final String modelUrl;17 final Color? backgroundColor;18 final bool autoPlay;19 final double width;20 final double height;2122 @override23 State<Model3DViewer> createState() => _Model3DViewerState();24}2526class _Model3DViewerState extends State<Model3DViewer> {27 final Flutter3DController _controller = Flutter3DController();2829 @override30 Widget build(BuildContext context) {31 return SizedBox(32 width: widget.width,33 height: widget.height,34 child: Flutter3DViewer(35 src: widget.modelUrl,36 controller: _controller,37 progressBarColor: Colors.blue,38 onLoad: () {39 if (widget.autoPlay) _controller.playAnimation();40 },41 onError: (error) {42 debugPrint('3D model error: $error');43 },44 ),45 );46 }47}Expected result: A 3D model renders in the widget area. The user can drag to rotate and pinch to zoom. If the model has animation, it plays automatically.
Enable Google Cardboard split-screen mode
Enable Google Cardboard split-screen mode
Google Cardboard works by displaying two side-by-side views of the same scene with a slight offset — this creates the stereoscopic 3D illusion when viewed through the Cardboard lenses. For a panorama viewer, this means rendering the same 360° scene twice with slightly different viewpoints. Add the google_cardboard package or implement a simple split-screen layout manually: a Row widget with two Expanded children, each containing a PanoramaViewer360 widget with the same imageUrl. Set the rotation offset on the right viewer to a small positive value (3-5 degrees) relative to the left viewer using a shared App State variable tracking the current pan angle.
Expected result: Two side-by-side panorama views display on screen. When placed in a Google Cardboard viewer, the scene appears 3D.
Build a virtual tour with multiple panorama hotspots
Build a virtual tour with multiple panorama hotspots
A virtual tour links multiple 360° panoramas together through interactive hotspots — tapping an arrow in the panorama loads the next room's panorama. Create an App State variable 'currentPanoramaUrl' (String). Stack the PanoramaViewer360 widget with a Row of IconButton widgets positioned at the panorama navigation points. Each button updates currentPanoramaUrl to the next room's image URL. Wrap the buttons in Positioned widgets inside the Stack to place them at specific screen locations. Add a fade transition animation on the PanoramaViewer360 widget when currentPanoramaUrl changes for a smooth room transition.
Expected result: A multi-room virtual tour where tapping navigation arrows smoothly transitions between panoramic views of different rooms.
Complete working example
1// FlutterFlow Custom Widget: PanoramaTour2// A virtual tour with multiple connected panoramas and hotspot navigation3// Package: panorama_viewer ^1.0.04import 'package:flutter/material.dart';5import 'package:panorama_viewer/panorama_viewer.dart';67class PanoramaTour extends StatefulWidget {8 const PanoramaTour({9 super.key,10 required this.scenes, // List of {url, label} maps11 this.initialIndex = 0,12 this.height = 400.0,13 });1415 final List<dynamic> scenes;16 final int initialIndex;17 final double height;1819 @override20 State<PanoramaTour> createState() => _PanoramaTourState();21}2223class _PanoramaTourState extends State<PanoramaTour>24 with SingleTickerProviderStateMixin {25 late int _currentIndex;26 late AnimationController _fadeController;27 late Animation<double> _fadeAnimation;2829 @override30 void initState() {31 super.initState();32 _currentIndex = widget.initialIndex;33 _fadeController = AnimationController(34 vsync: this,35 duration: const Duration(milliseconds: 400),36 );37 _fadeAnimation = CurvedAnimation(38 parent: _fadeController,39 curve: Curves.easeInOut,40 );41 _fadeController.value = 1.0;42 }4344 @override45 void dispose() {46 _fadeController.dispose();47 super.dispose();48 }4950 Future<void> _navigateTo(int index) async {51 if (index < 0 || index >= widget.scenes.length) return;52 await _fadeController.reverse();53 setState(() => _currentIndex = index);54 await _fadeController.forward();55 }5657 @override58 Widget build(BuildContext context) {59 final scene = widget.scenes[_currentIndex];60 final hasPrev = _currentIndex > 0;61 final hasNext = _currentIndex < widget.scenes.length - 1;6263 return SizedBox(64 height: widget.height,65 child: Stack(66 children: [67 FadeTransition(68 opacity: _fadeAnimation,69 child: PanoramaViewer(70 sensorControl: SensorControl.absoluteOrientation,71 child: Image.network(72 scene['url'] as String,73 fit: BoxFit.cover,74 ),75 ),76 ),77 // Room label78 Positioned(79 top: 16, left: 16,80 child: Container(81 padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),82 decoration: BoxDecoration(83 color: Colors.black54,84 borderRadius: BorderRadius.circular(20),85 ),86 child: Text(87 scene['label'] as String? ?? '',88 style: const TextStyle(color: Colors.white, fontSize: 14),89 ),90 ),91 ),92 // Navigation arrows93 Positioned(94 bottom: 24,95 left: 0, right: 0,96 child: Row(97 mainAxisAlignment: MainAxisAlignment.center,98 children: [99 if (hasPrev)100 FloatingActionButton.small(101 heroTag: 'prev',102 onPressed: () => _navigateTo(_currentIndex - 1),103 backgroundColor: Colors.black54,104 child: const Icon(Icons.arrow_back, color: Colors.white),105 ),106 const SizedBox(width: 16),107 if (hasNext)108 FloatingActionButton.small(109 heroTag: 'next',110 onPressed: () => _navigateTo(_currentIndex + 1),111 backgroundColor: Colors.black54,112 child: const Icon(Icons.arrow_forward, color: Colors.white),113 ),114 ],115 ),116 ),117 ],118 ),119 );120 }121}Common mistakes when creating a Virtual Reality VR Experience in FlutterFlow
Why it's a problem: Promising clients true VR headset support when building with FlutterFlow
How to avoid: Be explicit: FlutterFlow delivers mobile VR (360° views, Google Cardboard) not immersive headset VR. For true headset VR, clients need Unity or Unreal Engine, which is a separate and significantly more complex project.
Why it's a problem: Testing panorama gyroscope navigation in the web browser
How to avoid: Always test panorama and VR features on a physical device. Use the QR code in Run Mode to open the preview on your phone and test the gyroscope tilting behavior.
Why it's a problem: Using regular photos instead of 360° equirectangular images
How to avoid: Only use images explicitly formatted as equirectangular 360° panoramas. Download test images from polyhaven.com or shoot with a 360° camera app.
Best practices
- Be upfront with clients that Flutter VR means mobile VR (360°/Cardboard), not headset VR
- Compress 360° images to under 3MB before uploading — large images cause slow load and out-of-memory crashes on older phones
- Always provide a 2D fallback view (regular photo) for users on web or devices without gyroscopes
- Store panorama scene data in Firestore to update tours without app updates
- Use loading indicators while panorama images download — a blank screen while loading frustrates users
- Test on a mid-range Android device (not just the latest iPhone) — 3D rendering performance varies significantly
- For commercial virtual tours, consider dedicated 360° tour platforms (Matterport, Kuula) that handle hosting and analytics
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a virtual property tour in FlutterFlow using panorama_viewer. I have 5 equirectangular 360° images stored in Firebase Storage. I want a Flutter Custom Widget that displays the current panorama with gyroscope navigation, and shows navigation buttons to move to the next/previous room with a fade transition between rooms. The widget should accept a list of scene objects (each with url and roomName) and the initial room index.
In FlutterFlow, I want to create a virtual product showcase that shows a 3D model (.glb file) that users can rotate and zoom. The model URL is stored in a Firestore document field called 'modelUrl'. How do I set up the flutter_3d_controller Custom Widget, pass the Firestore field value as a parameter, and display it on a product detail page?
Frequently asked questions
Does FlutterFlow support Oculus or Meta Quest headsets?
No. Flutter has no Meta Quest, Oculus, or any VR headset SDK. The Meta Quest runs Android under the hood, but Meta's VR SDK is specific to their platform and not integrated with Flutter's rendering pipeline. For headset VR, you need Unity or Unreal Engine.
Does Flutter support Apple Vision Pro?
As of early 2026, Flutter does not have official visionOS support. Apple Vision Pro runs on visionOS, which requires native SwiftUI or RealityKit development. Flutter cannot deploy to visionOS. This is on the Flutter team's long-term roadmap but has no confirmed timeline.
What is the difference between AR and VR in FlutterFlow?
AR (augmented reality) overlays digital content on a real-world camera feed. Flutter supports AR through the ar_flutter_plugin package, which works on iOS ARKit and Android ARCore. VR (virtual reality) is a fully synthetic environment — 360° panoramas in Flutter, not headset VR. Both require Custom Widgets and the Pro plan.
Can I use WebVR (A-Frame) inside FlutterFlow?
Yes, indirectly. You can use a WebView widget to load an A-Frame or Three.js WebVR page inside your FlutterFlow app. This is a workaround that embeds a web VR experience inside a native Flutter shell. It works but adds load time and limits interaction between the WebVR content and your Flutter app's state.
What 360° camera app should I use to take photos for my virtual tour?
For iPhone, use the built-in panorama mode or the Google Street View app (exports equirectangular). For Android, Google Street View and Panorama 360° apps work well. For professional virtual tours, a dedicated camera like Insta360 or Ricoh Theta produces much higher quality equirectangular images.
Will my 360° panorama work on web browsers or only on phones?
panorama_viewer works on mobile (iOS/Android) with full gyroscope support. On web, it works with mouse/touch drag but without gyroscope. The gyroscope-based navigation (tilting your phone to look around) requires a physical device. Cardboard split-screen mode only makes sense on a physical device held in a headset.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation