# How to create a custom webview for your FlutterFlow app

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 20-30 min
- Compatibility: FlutterFlow Pro+ (Custom Code required)
- Last updated: March 2026

## TL;DR

FlutterFlow has no built-in WebView widget, so you need a Custom Widget using the webview_flutter package. This tutorial shows you how to create the Custom Widget with WebViewController, pass a URL via Component Parameter, enable JavaScript, add a loading spinner overlay, and handle navigation callbacks — all in about 60 lines of Dart.

## Embedding a WebView in FlutterFlow

FlutterFlow does not include a built-in WebView widget. To embed external web pages, payment forms, or web-based tools inside your app, you need to create a Custom Widget using the webview_flutter package from pub.dev. This tutorial walks you through the full setup: adding the dependency, writing the Dart code, passing configuration via Component Parameters, and handling common issues like loading states and JavaScript communication.

## Before you start

- FlutterFlow Pro plan or higher (Custom Code required)
- A FlutterFlow project open in the builder
- The URL you want to embed (must be HTTPS for iOS)
- Basic understanding of what a Custom Widget is in FlutterFlow

## Step-by-step guide

### 1. Add the webview_flutter dependency to your project

In FlutterFlow, go to Custom Code in the left Navigation Menu. Click the Pubspec Dependencies tab. Add webview_flutter with version ^4.4.0. Click Save. This makes the package available for import in any Custom Widget or Custom Action. FlutterFlow will download the package on next compile.

**Expected result:** The webview_flutter package appears in your Pubspec Dependencies list without errors.

### 2. Create the Custom Widget with WebView controller

Go to Custom Code → Custom Widgets → click + Add. Name it CustomWebView. Set width parameter to double.infinity and height to 500 (or pass via parameter). In the code editor, import webview_flutter. Create a StatefulWidget. In initState(), initialize the WebViewController: set JavaScriptMode.unrestricted, set the NavigationDelegate to filter URLs (optional), and call loadRequest(Uri.parse(widget.url)). In build(), return WebViewWidget(controller: _controller).

```
import 'package:webview_flutter/webview_flutter.dart';

class CustomWebView extends StatefulWidget {
  const CustomWebView({super.key, this.width, this.height, required this.url, this.onPageFinished});
  final double? width;
  final double? height;
  final String url;
  final Future Function(String url)? onPageFinished;

  @override
  State<CustomWebView> createState() => _CustomWebViewState();
}

class _CustomWebViewState extends State<CustomWebView> {
  late final WebViewController _controller;
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(NavigationDelegate(
        onPageFinished: (url) {
          setState(() => _isLoading = false);
          widget.onPageFinished?.call(url);
        },
      ))
      ..loadRequest(Uri.parse(widget.url));
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width,
      height: widget.height ?? 500,
      child: Stack(
        children: [
          WebViewWidget(controller: _controller),
          if (_isLoading)
            const Center(child: CircularProgressIndicator()),
        ],
      ),
    );
  }
}
```

**Expected result:** The Custom Widget compiles without errors. You see CustomWebView in your widget palette.

### 3. Add Component Parameters for URL and callbacks

In the Custom Widget editor, ensure you have these parameters defined: url (String, required) — the web page URL to load, and onPageFinished (Action, optional) — callback triggered when page finishes loading. Click Compile Code to verify. The url parameter lets you reuse this widget across pages with different URLs by passing the value from the parent.

**Expected result:** Parameters show in the Properties Panel when you place the widget on a page.

### 4. Place the widget on a page and pass the URL

Navigate to the page where you want the WebView. Drag your CustomWebView widget from the Widget Palette (under Custom Widgets). In the Properties Panel, set the url parameter — either hardcode a URL string or bind it to a Page State variable or Route Parameter for dynamic URLs. Set width to infinity (fills parent) and height to the desired pixel value. Wrap in an Expanded widget if it is inside a Column and should fill remaining space.

**Expected result:** In Run mode, the page loads the specified URL inside the embedded WebView with a loading spinner.

### 5. Add JavaScript communication for web-to-app data passing

To receive data from the web page (e.g., a payment confirmation or form result), add a JavaScriptChannel to the controller in initState(). The channel creates a global JavaScript object that the web page can call: window.FlutterChannel.postMessage('data'). In the handler, parse the message and update Page State or trigger an Action Parameter callback.

```
_controller
  ..addJavaScriptChannel(
    'FlutterChannel',
    onMessageReceived: (message) {
      // message.message contains the string from JS
      widget.onDataReceived?.call(message.message);
    },
  );
```

**Expected result:** Web page can send data to your FlutterFlow app via the JavaScript channel.

## Complete code example

File: `custom_web_view.dart`

```dart
import 'package:webview_flutter/webview_flutter.dart';

class CustomWebView extends StatefulWidget {
  const CustomWebView({
    super.key,
    this.width,
    this.height,
    required this.url,
    this.onPageFinished,
    this.onDataReceived,
  });

  final double? width;
  final double? height;
  final String url;
  final Future Function(String url)? onPageFinished;
  final Future Function(String data)? onDataReceived;

  @override
  State<CustomWebView> createState() => _CustomWebViewState();
}

class _CustomWebViewState extends State<CustomWebView> {
  late final WebViewController _controller;
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..addJavaScriptChannel(
        'FlutterChannel',
        onMessageReceived: (message) {
          widget.onDataReceived?.call(message.message);
        },
      )
      ..setNavigationDelegate(NavigationDelegate(
        onPageStarted: (_) => setState(() => _isLoading = true),
        onPageFinished: (url) {
          setState(() => _isLoading = false);
          widget.onPageFinished?.call(url);
        },
        onWebResourceError: (error) {
          setState(() => _isLoading = false);
          debugPrint('WebView error: ${error.description}');
        },
      ))
      ..loadRequest(Uri.parse(widget.url));
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width ?? double.infinity,
      height: widget.height ?? 500,
      child: Stack(
        children: [
          WebViewWidget(controller: _controller),
          if (_isLoading)
            const Center(child: CircularProgressIndicator()),
        ],
      ),
    );
  }
}
```

## Common mistakes

- **Using http:// URLs on iOS** — iOS App Transport Security blocks non-HTTPS requests by default. The WebView shows a blank white screen with no error message in the app UI. Fix: Always use https:// URLs. If you must load HTTP after code export, add an ATS exception in ios/Runner/Info.plist — but this is not recommended for production.
- **Not setting a fixed height on the WebView container** — WebView inside a Column without height constraints causes a RenderBox layout error because WebView tries to be infinitely tall. Fix: Always set an explicit height on the Custom Widget or wrap it in Expanded when inside a Column so it takes the remaining available space.
- **Forgetting to enable JavaScript** — Many websites and all payment forms require JavaScript. Without JavaScriptMode.unrestricted, interactive elements don't work and forms fail silently. Fix: Set JavaScriptMode.unrestricted in the WebViewController setup. Only disable JS if loading purely static content for security.

## Best practices

- Always pass the URL as a Component Parameter — never hardcode URLs in the Dart code
- Show a loading indicator overlay while the page loads using the onPageStarted/onPageFinished callbacks
- Use NavigationDelegate to block navigation to external domains if the WebView should stay on one site
- Handle onWebResourceError to show a user-friendly error message instead of a blank screen
- Test on both iOS and Android — WebView behavior differs between platforms (iOS uses WKWebView, Android uses Chrome)
- For payment forms, prefer Stripe Checkout redirect over embedding Stripe Elements in a WebView
- Dispose the WebView when not needed to free memory — avoid keeping it alive in a background tab

## Frequently asked questions

### Can I use FlutterFlow's built-in widgets instead of Custom Code for WebView?

No. FlutterFlow does not have a built-in WebView widget as of March 2026. You must create a Custom Widget using the webview_flutter package, which requires a Pro plan or higher.

### Does the WebView work on web deployments?

The webview_flutter package only works on iOS and Android. For web, use an HtmlElementView with an iframe instead — this requires a separate Custom Widget for web platform. Or use Launch URL to open in a new browser tab.

### How do I pass data FROM my app TO the web page?

Use controller.runJavaScript('functionName("data")') to call a JavaScript function on the loaded page. The web page must define that function to handle the incoming data.

### Can the WebView access the device camera or location?

WebView has limited access to device features. Camera and file upload work on Android via onShowFileChooser but require additional configuration on iOS. Location works if the web page requests it and the app has location permissions.

### Why is my WebView showing a blank white screen?

Common causes: HTTP URL blocked by iOS ATS (use HTTPS), JavaScript disabled (enable unrestricted mode), URL returns an error (check in browser first), or WebView has zero height (set explicit height).

### Can RapidDev help with complex WebView integrations?

Yes. If you need OAuth flows inside WebView, deep JavaScript bridging, or platform-specific WebView configurations, RapidDev's team can build Custom Widgets that handle these advanced scenarios.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-custom-webview-for-your-flutterflow-app
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-custom-webview-for-your-flutterflow-app
