Canvas
Rendering pixels on-screen
Canvas follows the semantics of the Web <canvas> element as closely as we can: call getContext("webgpu") to get the GPUCanvasContext, read its size synchronously, manage pixel density the same way, and resize through the same clientWidth / clientHeight model, so browser WebGPU code largely carries over. Where a platform forces a difference we re-surface it rather than hide it. The primary example is frame presentation: on the Web the browser swaps the rendered frame automatically, but on React Native you call present() yourself after submitting your commands. Compositing a transparent canvas over native views on Android (see Transparent compositing) is another.
Props
| Prop | Type | Description |
|---|---|---|
opaque | boolean | Defaults to true. false alpha-composites the canvas over the views behind it (Android and web; iOS uses alphaMode alone) |
android | AndroidCanvasProps | Android-only rendering options: surfaceType and zOrderOnTop. Ignored on iOS and web |
...ViewProps | ViewProps | All standard React Native View props (style, onLayout, etc.) |
Usage
Minimal on-screen rendering loop:
import { , } from "react";
import { , StyleSheet, } from "react-native";
import { , , type CanvasRef } from "react-native-webgpu";
const = `
@vertex fn main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
var pos = array<vec2f, 3>(vec2(0, 0.5), vec2(-0.5, -0.5), vec2(0.5, -0.5));
return vec4f(pos[i], 0.0, 1.0);
}`;
const = `@fragment fn main() -> @location(0) vec4f {
return vec4(1.0, 0.0, 0.0, 1.0);
}`;
export function () {
const = <CanvasRef>(null);
const { } = ();
(() => {
if (!) return;
let = false;
let = 0;
const = () => {
const = .?.("webgpu");
if (! || ) {
= ();
return;
}
const = . as HTMLCanvasElement;
if (. === 0 || . === 0) {
= ();
return;
}
const = ..();
. = . * .();
. = . * .();
.({ , , : "premultiplied" });
const = .({
: "auto",
: {
: .({ : }),
: "main",
},
: {
: .({ : }),
: "main",
: [{ }],
},
: { : "triangle-list" },
});
const = () => {
if () return;
const = .();
const = .({
: [{
: .().(),
: [0, 0, 0, 0],
: "clear",
: "store",
}],
});
.();
.(3);
.();
..([.()]);
.();
= ();
};
= ();
};
= ();
return () => {
= true;
();
};
}, []);
return (
< ={{ : 1 }}>
< ={{ : 1, : "#3498db" }} />
< ={} ={StyleSheet.} ={false} />
</>
);
}Steps in order:
- Mount
<Canvas ref={ref} />and wait for layout (non-zeroclientWidth/clientHeight). - Request a device with
useDeviceorGPUDeviceProvider. - Call
ref.current.getContext("webgpu")andcontext.configure({ device, format, alphaMode }). - Each frame: encode passes →
device.queue.submit()→context.present().
See Canvas for the frame loop and Learn WebGPU for a step-by-step walkthrough.
Presenting frames
On the Web, the browser swaps your rendered frame to the screen automatically when you submit GPU commands. In React Native there is no automatic swap: presentation is a manual step. After you submit your commands to the queue, call present() on the context to display the frame.
..([.()]);
.(); // React Native onlypresent() is a React Native only method (it is not part of the Web WebGPU spec). It runs synchronously on the calling thread, so the frame is presented from whichever thread did the rendering. This works the same on every runtime: the main JS runtime, the UI thread, and dedicated worklet runtimes (createWorkletRuntime / runOnRuntime, or a Vision Camera frame processor).
Always call present() after submit(), never before. See Frame presentations for the full frame loop and Native APIs for threading details.
Pixel density
Just like on the Web, a canvas has two independent sizes:
clientWidth/clientHeightare the on-screen (layout) size in logical points. They follow the view's layout and update automatically when it resizes.width/heightare the drawing-buffer size in physical pixels. This is the resolution WebGPU actually renders at.
The drawing buffer is not scaled to the device pixel ratio by default, exactly as on the Web. To render crisply on high-density screens, set the buffer size to the layout size multiplied by the pixel ratio. PixelRatio.get() is the React Native equivalent of window.devicePixelRatio.
const = . as HTMLCanvasElement;
. = . * .();
. = . * .();Because clientWidth / clientHeight track layout, recompute the buffer size whenever they change (for example on rotation or a resize). See Pixel density for a resize-aware render loop.
Transparent compositing
To render the canvas over the React Native views beneath it, you control transparency from two places, and you normally set both:
opaque={false}on<Canvas>makes the underlying native surface translucent so the views behind it show through.alphaMode: "premultiplied"oncontext.configure, together with a clear color whose alpha is0, makes WebGPU produce a transparent frame.
<Canvas ref={ref} style={StyleSheet.absoluteFill} opaque={false} />The Usage example above wires up both: opaque={false}, alphaMode: "premultiplied", and a clearValue of [0, 0, 0, 0].
Platform note: on Android the alphaMode passed to configure() is ignored, so the Android backing view controls surface transparency and opaque is what selects it. On iOS alphaMode alone controls it and opaque is ignored. Setting opaque={false} together with alphaMode: "premultiplied" and an alpha-0 clear color gives you the same result on both platforms.
Android rendering options
Android has no single view type that is both cheap and composited like a
regular view, so Canvas can be backed by one of three native views. The
android prop selects it; it is ignored on iOS and web.
| Option | Type | Effect |
|---|---|---|
surfaceType | "SurfaceView" | "TextureView" | "HardwareBufferView" | Backing view. Defaults to SurfaceView when opaque and to TextureView otherwise. HardwareBufferView is opt-in and needs Android 10 (API 29) |
zOrderOnTop | boolean | SurfaceView only: setZOrderOnTop, composites above every React Native view in the window. Defaults to false |
SurfaceView is the default for an opaque canvas and the fastest path.
The frame goes straight to the system compositor as its own layer, with no
extra copy and no involvement of the React Native view hierarchy. The price is
that it is not really view content: it punches a hole through the window, so
parent transforms, clipping, rounded corners, and z-ordering with sibling views
do not apply to it. Use it whenever the canvas is a plain opaque rectangle.
TextureView is the default for a non-opaque canvas. It is a regular
Android view: frames go through a SurfaceTexture that the UI toolkit samples
as an external texture when it draws the view, so parent transforms, clipping,
alpha, and z-order all apply and the canvas can be composited over other React
Native views. That routing costs an extra texture copy and typically a frame of
latency, and it can stall during some view animations. It works on every
supported Android version.
HardwareBufferView is an opt-in, experimental alternative to
TextureView for the non-opaque case, available on Android 10 (API 29) and
above. It is also a regular view, so it composites exactly like TextureView,
but WebGPU renders into an AHardwareBuffer that the UI toolkit draws inline,
with no SurfaceTexture and no extra copy. It is designed to have lower
latency than TextureView while keeping the same compositing behavior; that
gain has not been measured yet, which is one reason it is not the default.
Requesting it on a device below API 29 silently uses TextureView.
SurfaceView | TextureView | HardwareBufferView | |
|---|---|---|---|
| Minimum Android | any | any | 10 (API 29) |
| Composited like a regular view | no | yes | yes |
| Extra copy | none | one | none |
| Expected latency | lowest | highest | between the two (unmeasured) |
| Selected when | opaque (default) | opaque={false} (default) | surfaceType: "HardwareBufferView" |
opaque applies to whichever view is selected: on a SurfaceView it picks
PixelFormat.OPAQUE or PixelFormat.TRANSLUCENT, on a TextureView it calls
setOpaque, and a HardwareBufferView always composites with alpha. Changing
opaque at runtime updates the view in place; changing surfaceType or
zOrderOnTop replaces the backing view.
The defaults composite correctly in React Native stacking order without
further flags. Set surfaceType when you need a different trade-off:
// Opaque TextureView: stays in stacking order, e.g. inside a ScrollView
<Canvas android={{ surfaceType: "TextureView" }} />
// Translucent SurfaceView above every React Native view, no composition pass
<Canvas opaque={false} android={{ surfaceType: "SurfaceView", zOrderOnTop: true }} />
// Translucent HardwareBufferView, opt-in (uses TextureView before Android 10)
<Canvas opaque={false} android={{ surfaceType: "HardwareBufferView" }} />Two things to know before opting into HardwareBufferView. If the device
cannot allocate or import the AHardwareBuffer, the canvas currently keeps
rendering to an offscreen texture and shows nothing, rather than falling back
to TextureView; test on your target devices. And because the UI toolkit
exposes no "finished reading" signal for an inline buffer, it recycles its
buffers with a small holding ring rather than a fence, so a device or driver
quirk could show up as a stale or torn frame. If either happens, drop the
surfaceType to get TextureView back. The plan is to make
HardwareBufferView the non-opaque default once it falls back to TextureView
at runtime and its latency advantage is measured.
A non-opaque SurfaceView without zOrderOnTop sits below the app window, so
its alpha blends against the window background (usually black) and the React
Native views beneath it are punched out. Pair it with zOrderOnTop to
composite over React Native content, or use one of the two view-based
backends. zOrderOnTop draws above all React Native views in the window,
including navigation screens, regardless of zIndex.
See also
- GPUDeviceProvider - share one device across components
- Canvas - the rendering surface and frame loop
- Learn WebGPU - tutorials for the low-level API