Next.js + React Three Fiber + React Three XR + Variant Launch to develop a WebXR compatible AR app on iOS!

Next.js + React Three Fiber + React Three XR + Variant Launch to develop a WebXR compatible AR app on iOS!

I was thinking of creating an AR app using WebXR, but the current situation is that iOS does not officially support WebXR

So, I used Variant Launch, which was also featured in the official React Three XR, to support WebXR AR on iOS as well.

Does it work on iOS?
WebXR for VR experiences is supported on Safari for Apple Vision Pro. WebXR is not supported on iOS Safari yet. The alternative is to use products such as Variant Launch, which allow to build WebXR experiences for iOS.

-translation

Does it work on iOS?
WebXR for VR experiences is supported in Safari on Apple Vision Pro. WebXR is not supported on iOS Safari. Instead, you can use products such as Variant Launch, which allows you to build a WebXR experience for iOS.

Although there are some examples of implementations using Three.js alone, few implementations using React Three Fiber were found, and there weren't many examples of configurations using Next.js, so I'll summarize them here.

There were a few things to get hooked on during the implementation, so I hope this will be helpful.

table of contents

What is Variant Launch?

Variant Launch is a solution for implementing WebXR on iOS, even though iOS Safari does not officially support WebXR.

The key to the mechanism is that it uses App Clip Rather than running WebXR on iOS Safari as a regular web app, we launch a dedicated native component via App Clip and provide the WebXR API on top of it. This allows you to obtain camera access and sensor information that cannot be used with normal iOS restrictions, and can be used from the web as a standard WebXR compatible API.

App Clip is a pre-installed feature on iOS, and users do not need to download the app in advance. App Clip will be called directly from the web page and the WebXR session will start immediately.

If the target app (special app for Variant Launch) is already installed, WebXR will run on that app rather than on the App Clip.

List of libraries used

The libraries used in this project are as follows:
Library versions and libraries used for development purposes other than WebXR are also listed in package.json

  • Next.js
    React framework. In order to avoid server-side rendering (SSR), "use client" and dynamic import to limit the drawing part of WebXR to the client side.
  • React Three Fiber
    A wrapper for handling Three.js with React. Three.js rendering and scene management can be handled in the form of React components.
  • React Three XR
    An additional library for working with WebXR on React Three Fiber. Easily manage your WebXR sessions using components and createXRStore
  • Three.js
    Core library for 3D drawing. Three.js runs inside React Three Fiber, and is normally operated via Fiber components. If necessary, you can use the low-level API of Three.js directly, but in this implementation, it is mainly used via Fiber.
  • Variant Launch
    A service for running WebXR on iOS. It provides WebXR compatible APIs using App Clips and dedicated apps internally.
  • Vercel
    This time, we will use Vercel as the deployment destination.
    Set up the domain that will be issued when deployed to a Variant Launch domain.

For other libraries and versions, please read package.json.
*The latest version did not work well, so I lowered the version a little.

{ "name": "glb-ar-viewer", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev --turbopack", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "@react-three/drei": "^10.1.2", "@react-three/fiber": "^9.1.2", "@react-three/xr": "^6.6.17", "next": "15.3.3", "react": "^19.0.0", "react-dom": "^19.0.0", "three": "^0.171.0" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.3", "tailwindcss": "^4", "typescript": "^5" } }

Environmental construction

Regarding environment construction, I created a template using create-next-app

npx create-next-app@latest glb-ar-viewer npm install three @react-three/fiber @react-three/xr @react-three/drei

Basically, this command should be fine, but if an error occurs when displaying AR, it's a good idea to lower the three version.

Implementing a WebXR-AR app for iOS

Once the environment is built, I would like to actually implement an iOS-compatible WebXR-AR app.
The folder structure is as follows:

glb-ar-viewer/ ├── .next/ ├── node_modules/ ├── public/ ├── src/ ├── app/ │ ├── favicon.ico │ ├── globals.css │ │ ├── layout.tsx │ │ └── pages.tsx │ └── components/ │ └── ARCanvas.tsx ├── .gitignore ├── eslint.config.mjs ├── next-env.d.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── README.md └── tsconfig.json

If there are any other places you want to see, please check GitHub.

app/globals.css: Configuring background transparency

In WebXR AR, the background must be transparent and the camera image is displayed behind the scenes.
This time, I used TailwindCSS and set up the CSS as follows:

If it's next.js, I think it's fine if you could specify "globals.css" as follows.

@import "tailwindcss"; :root { --background: transparent; --foreground: #171717; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } @media (prefers-color-scheme: dark) { :root { --background: #0a0a0a; --foreground: #ededed; } } body { background: var(--background); color: var(--foreground); font-family: Arial, Helvetica, sans-serif; }

app/layout.tsx: Preload Variant Launch SDK with Script tag

To use Variant Launch, preload the Variant Launch SDK with a Script tag in layout.tsx.

import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import Script from "next/script"; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], }); export const metadata: Metadata = { title: "GLB AR Viewer", description: "Application to display GLB models in AR", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return (<html lang="en"><body className={`${geistSans.variable} ${geistMono.variable} antialiased`}> {children}</body> {/* Loading Variant Launch SDK */}<Script src="https://launchar.app/sdk/v1?key=xxxxxx&redirect=true" strategy="beforeInteractive" /> </html> ); }

The points are as follows: If you can set it properly, it will automatically redirect to Variant Launch on iOS.

  • strategy="beforeInteractive"
    → Specify to load the SDK before the client-side script. Required for the WebXR compatible API to properly grow.
  • redirect=true
    → Enable URL redirection for Variant Launch (for App Clip guiding processing at first startup)

app/page.tsx: View Canvas using dynamic import

the Canvas part is loaded only on the client side using dynamic import to prevent WebXR-related code from being evaluated during SSR (server-side rendering)

"use client"; import dynamic from "next/dynamic"; const ARCanvas = dynamic(() => import("@/components/ARCanvas"), { ssr: false, }); export default function Page() { return<ARCanvas /> ; }

By setting
ssr: false If it becomes an SSR, it won't work in Variant Launch, so be careful.

components/ARCanvas.tsx: Implementation of AR display part

WebXR session management createXRStore local , hit-test , and dom-overlay are specified as requiredFeatures required when starting a session Canvas is React Three Fiber Draw with a component.

"use client"; import { Canvas } from "@react-three/fiber"; import { XR, createXRStore } from "@react-three/xr"; import { useState } from "react"; export default function ARCanvas() { const [red, setRed] = useState(false); const [store] = useState(() => createXRStore({ customSessionInit: { requiredFeatures: ["local", "hit-test", "dom-overlay"], optionalFeatures: ["anchors"], domOverlay: { root: document.body }, }, }) ); const handleEnterAR = async () => { if (store) { await store.enterAR(); } }; return (<div className="w-screen h-screen relative"><div className="absolute top-4 left-4 z-10 flex gap-4"> <button onClick={handleEnterAR} className="p-3 bg-white text-black rounded" >Enter AR</button></div><Canvas style={{ backgroundColor: "transparent" }} onCreated={({ gl }) => { gl.xr.enabled = true; gl.xr.setReferenceSpaceType("local"); }} > <XR store={store}><ambientLight /><directionalLight position={[1, 2, 3]} /><mesh pointerEventsType={{ deny: "grab" }} onClick={() => setRed(!red)} position={[0, 1, -1]} ><boxGeometry /><meshBasicMaterial color={red ? "red" : "blue"} /></mesh></XR></Canvas></div> ); }

You just need to follow the SDK guide for Variant Launch, but there is no procedure using React Three XR's createXRStore, so just read it and it's fine.

Summary of notes

I think the behavior will vary depending on the version, but as a memo I will organize the points I've gotten into.

Errors due to SSR (server-side rendering)

Next.js performs SSR by default, so referencing a document or window will result in
a ReferenceError WebXR is basically a client-side API, so all the drawing parts are specified as "use client" dynamic import to disable SSR.

*Android also works with SSR, so if it doesn't work on iOS, you should check it out.

const ARCanvas = dynamic(() => import("@/components/ARCanvas"), { ssr: false, });

gl.xr.setReferenceSpaceType("local") is explicitly specified

You can indirectly specify the referenceSpaceType in createXRStore it may not be automatically reflected on the gl.xr This is explicitly specified for stability in iOS and Variant Launch.

I don't know the exact reason, but I couldn't see it on iOS unless I specified this.

gl.xr.setReferenceSpaceType("local");

Background Transparency Settings

When performing AR with WebXR, camera footage will not be visible unless the background is transparent. The body is set to transparent in CSS backgroundColor: "transparent" specified in Canvas.

:root { --background: transparent; } body { background: var(--background); }

Preloading the Variant Launch SDK

To use the WebXR compatible API on iOS, I have preloaded the Variant Launch SDK. Write it in
Next.js RootLayout Just write this and it will automatically redirect you to Variant Launch.

<Script src="https://launchar.app/sdk/v1?key=..." strategy="beforeInteractive" />

Finally, check the operation (confirmed with iPhone 16)

I checked the operation on my favorite iPhone 16. (We have confirmed that it works fine on Android too.)

The AR display is successfully displayed as shown below! Of course, I've already tapped it!

iOS does not yet support WebXR, but I found out that using Variant Launch allows you to use WebXR on iOS pseudo-only.
It's a bit of a hassle, but Variant Launch offers 3,000 views for free, so be sure to give it a try!

It would be great if iOS could support WebXR as well. . .

Share if you like!

Who wrote this article

This is a blog I started to study information security. As a new employee, I would be happy if you could look with a broad heart.
There is also Teech Lab, which is an opportunity to study programming fun, so if you are interested in software development, be sure to take a look!

table of contents