Skip to content
Tauri

Multi-Window on Mobile

Tauri supports multiple windows on Android and iOS, allowing your app to display content side-by-side on tablets or in separate scenes on iPad.

On Android, multi-window is implemented using Activity Embedding, which lets the system display two activities side by side on large screens.

On iOS, multi-window uses the UIScene API, which allows iPad users to open multiple instances of your app in separate windows.

On phones, the system usually does not lay out two windows side by side. On Android, creating another window still launches a separate activity, but on handset-sized displays it is typically pushed onto the activity back stack—so Back returns to the previous activity instead of closing a split. On iOS (especially iPhone), opening or creating another window often replaces the current UI with the new scene’s content rather than keeping both visible at once; true concurrent windows remain an iPad (and Stage Manager) experience.

Both platforms require a capability permission to create new windows from the frontend.

Add the core:webview:allow-create-webview-window permission to your capability file so your frontend can create new windows.

If you are creating multiple windows, use a wildcard or list every window label in the windows array:

src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": ["core:default", "core:webview:allow-create-webview-window"]
}

Android multi-window uses Activity Embedding to split activities side by side on large screens (tablets, foldables). You need to create an Android Activity for each window type, configure split rules, and register an initializer.

  1. Add the required AndroidX libraries to your build.gradle.kts:

    src-tauri/gen/android/app/build.gradle.kts
    dependencies {
    // ... existing dependencies
    implementation("androidx.window:window:1.5.0")
    implementation("androidx.startup:startup-runtime:1.2.0")
    }
  2. Create a Kotlin class for each additional window type. Each activity must extend TauriActivity:

    src-tauri/gen/android/app/src/main/java/com/example/app/DetailActivity.kt
    package com.example.app
    import android.os.Bundle
    import android.os.PersistableBundle
    class DetailActivity: TauriActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    }
    }
  3. Register the new activity and enable activity embedding by adding the tools namespace and the embedding property:

    src-tauri/gen/android/app/src/main/AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application ...>
    <property
    android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
    android:value="true" />
    <!-- Existing MainActivity -->
    <activity
    android:name=".MainActivity"
    android:exported="true"
    ...>
    ...
    </activity>
    <!-- New activity for the detail window -->
    <activity android:name=".DetailActivity" android:exported="true" />
    <!-- Register the split initializer -->
    <provider android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data android:name="${applicationId}.SplitInitializer"
    android:value="androidx.startup" />
    </provider>
    </application>
    </manifest>
  4. The initializer loads the split pair rules at app startup:

    src-tauri/gen/android/app/src/main/java/com/example/app/SplitInitializer.kt
    package com.example.app
    import android.content.Context
    import androidx.startup.Initializer
    import androidx.window.core.ExperimentalWindowApi
    import androidx.window.embedding.RuleController
    @OptIn(ExperimentalWindowApi::class)
    class SplitInitializer : Initializer<RuleController> {
    override fun create(context: Context): RuleController {
    return RuleController.getInstance(context).apply {
    setRules(RuleController.parseRules(context, R.xml.main_split_config))
    }
    }
    override fun dependencies(): List<Class<out Initializer<*>>> {
    return emptyList()
    }
    }
  5. Create an XML resource that tells the system how to pair activities and split the screen:

    src-tauri/gen/android/app/src/main/res/xml/main_split_config.xml
    <resources
    xmlns:window="http://schemas.android.com/apk/res-auto">
    <SplitPairRule
    window:splitRatio="0.33"
    window:splitLayoutDirection="locale"
    window:splitMinWidthDp="840"
    window:splitMaxAspectRatioInPortrait="alwaysAllow"
    window:finishPrimaryWithSecondary="never"
    window:finishSecondaryWithPrimary="never"
    window:clearTop="false">
    <SplitPairFilter
    window:primaryActivityName=".MainActivity"
    window:secondaryActivityName=".DetailActivity"/>
    </SplitPairRule>
    </resources>

    Key attributes:

    • splitRatio — how the screen is divided (0.33 gives the primary activity one-third)
    • splitMinWidthDp — minimum screen width to activate the split (840dp targets tablets)
    • splitMaxAspectRatioInPortrait — set to alwaysAllow to enable split in portrait mode
    • primaryActivityName / secondaryActivityName — which activity pair triggers the split

On iOS, multi-window uses the UIScene API. iPad users can open new windows by long-pressing the app icon and selecting “New window”, or your app can create them programmatically.

  1. Create an Info.ios.plist file in your src-tauri directory to declare scene support:

    src-tauri/Info.ios.plist
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    <key>UIApplicationSceneManifest</key>
    <dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <true/>
    <key>UISceneConfigurations</key>
    <dict/>
    </dict>
    </dict>
    </plist>
  2. When a user requests a new window on iPad (for example by long-pressing the app icon), Tauri emits a RunEvent::SceneRequested event. Handle it to create a new window:

    src-tauri/src/lib.rs
    #[cfg_attr(mobile, tauri::mobile_entry_point)]
    pub fn run() {
    #[cfg(target_os = "ios")]
    let mut counter = 0;
    tauri::Builder::default()
    .setup(|app| {
    tauri::WebviewWindowBuilder::new(
    app, "main", tauri::WebviewUrl::default()
    ).build()?;
    Ok(())
    })
    .build(tauri::generate_context!())
    .expect("error while running tauri application")
    .run(move |app, event| {
    #[cfg(target_os = "ios")]
    if let tauri::RunEvent::SceneRequested { .. } = event {
    counter += 1;
    tauri::WebviewWindowBuilder::new(
    app,
    format!("main-{counter}"),
    tauri::WebviewUrl::default(),
    )
    .build()
    .unwrap();
    }
    #[cfg(not(target_os = "ios"))]
    let _ = (app, event);
    });
    }

You can create additional windows from both Rust and the frontend JavaScript API. The WebviewWindowBuilder (Rust) and WebviewWindow (JavaScript) accept platform-specific options:

Android options:

  • activityName — the name of the Android Activity class to create for this window.
  • createdByActivityName — the name of the Activity that is creating this window. This determines which activity stack the new activity belongs to, which is important for the split rules to work correctly. When not set, it is automatically inherited from the manager (e.g. when building from a Window or Webview handle).

iOS options:

  • requestedBySceneIdentifier — sets the identifier of the UIScene that is requesting the creation of this new scene, establishing a relationship between the two scenes. By default the system uses the foreground scene. When not set, it is automatically inherited from the manager.
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
function openDetail(id) {
const webview = new WebviewWindow(`detail-${id}`, {
url: `detail/${id}`,
activityName: 'DetailActivity',
});
webview.once('tauri://created', () => {
console.log('window created');
});
webview.once('tauri://error', (e) => {
console.error(e);
});
}

Once a window has been created, you can retrieve its platform-specific identifier:

const activityName = await window.activityName();
const sceneId = await window.sceneIdentifier();

These getters are useful for referencing a window’s identity when creating related windows. For example, you can read a window’s activityName to pass as createdByActivityName on a new window, or read sceneIdentifier to pass as requestedBySceneIdentifier.


© 2026 Tauri Contributors. CC-BY / MIT