WebGPU Triangle Demo: Start, Fix, and Keep It Honest
The first WebGPU triangle is the hardest one.
Not because the API is complicated, but because the pipeline has so many small gates. Adapter, device, canvas context, format, shader entry points, render pass, draw call — if any one of them is off, you get a blank canvas and no useful error.
This guide is the guide I wish I had when I started. It covers the minimal setup, the common failures, and a verification step after every stage so you know exactly where things break.
I cannot run these steps from this blog repo, so treat it as a lab guide. Open a local folder, follow each step, and check the outcome before moving on.
What you will build
By the end you will have:
- A canvas bound to WebGPU
- A requested adapter and device
- A minimal render pipeline
- One blue triangle on a dark background
- A loop that keeps it rendering
One honest triangle is worth more than a thousand copied demos.
WebGPU in five words
At its core, WebGPU is:
- Adapter: the GPU capability entry point.
- Device: the object that creates GPU resources.
- Pipeline: the configuration that ties shaders to output.
- Context: the bridge between WebGPU and the canvas.
- Render pass: the command that actually draws.
If those five pieces line up, you get a triangle. If one is missing or mismatched, you get silence.
What you need
- Chrome 113+, Edge, or any browser with stable WebGPU support
- A local static server (I use
npx serveor VS Code Live Server)
I recommend a local server because module scripts and WebGPU can behave differently when opened as a raw file:// URL.
Step 1: Create the HTML shell
Create index.html with a canvas and a module script:
<canvas id="gfx" width="640" height="360"></canvas><script type="module" src="main.js"></script>
Open it in the browser. You should see a blank page and no console errors. If you see a 404 for main.js, the script path is wrong. Fix that before continuing.
A blank canvas at this stage is success. It means the browser is ready for WebGPU, and we have not introduced any API code yet.
Step 2: Request the adapter and device
Create main.js and add the first WebGPU calls:
const adapter = await navigator.gpu.requestAdapter();if (!adapter) throw new Error("No GPU adapter");const device = await adapter.requestDevice();
This is the first real gate. requestAdapter asks the browser for a GPU. If it returns null, WebGPU is not enabled or not available. requestDevice gives you the object every later call depends on.
Open DevTools. No thrown error means you passed.
If you get No GPU adapter, check that your browser supports WebGPU and that any required flags are enabled. On some systems the integrated GPU is enough.
Step 3: Configure the canvas context
Now bind the canvas to WebGPU:
const canvas = document.getElementById("gfx");const context = canvas.getContext("webgpu");const format = navigator.gpu.getPreferredCanvasFormat();context.configure({ device, format, alphaMode: "opaque" });
The format returned by getPreferredCanvasFormat must match the format your pipeline targets later. Mismatched formats are one of the most common reasons for a blank screen. I use alphaMode: "opaque" because transparency is rarely needed for a first demo and can introduce subtle bugs.
No console errors here means the canvas is ready to receive frames.
If context is null, double-check the canvas ID and make sure the browser really supports WebGPU — not all canvas contexts do.
Step 4: Build a minimal pipeline
The pipeline connects your WGSL shader to the render pass. Start with the smallest shader possible:
const shaderCode = `@vertexfn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {var positions = array<vec2f, 3>(vec2f(0.0, 0.6),vec2f(-0.6, -0.6),vec2f(0.6, -0.6));return vec4f(positions[vi], 0.0, 1.0);}@fragmentfn fs_main() -> @location(0) vec4f {return vec4f(0.2, 0.6, 1.0, 1.0);}`;
Then create the pipeline:
const module = device.createShaderModule({ code: shaderCode });const pipeline = device.createRenderPipeline({layout: "auto",vertex: { module, entryPoint: "vs_main" },fragment: { module, entryPoint: "fs_main", targets: [{ format }] },primitive: { topology: "triangle-list" }});
I keep the shader self-contained because it removes one variable from debugging. If the triangle does not appear later, I know the problem is not a missing shader file or import path.
No shader compile errors in the console means the syntax is valid and the entry points were found.
Step 5: Draw the frame
The render pass is where everything finally connects:
function frame() {const encoder = device.createCommandEncoder();const pass = encoder.beginRenderPass({colorAttachments: [{view: context.getCurrentTexture().createView(),clearValue: { r: 0.05, g: 0.05, b: 0.08, a: 1 },loadOp: "clear",storeOp: "store"}]});pass.setPipeline(pipeline);pass.draw(3);pass.end();device.queue.submit([encoder.finish()]);requestAnimationFrame(frame);}frame();
loadOp: "clear" is important. Without it, the triangle can flicker or leave leftover pixels from previous frames. requestAnimationFrame keeps the loop running.
At this point you should see a blue triangle on a dark background. If you do, the entire pipeline is healthy.
Verify it worked
Check these three things:
- The triangle is visible.
- The console has no errors.
- The frame keeps rendering without flicker.
If all three are true, you have a solid baseline. Every later feature — uniforms, buffers, resize handling — builds on top of this.
Common mistakes
| Symptom | Likely cause | Fix |
|---|---|---|
navigator.gpu is undefined | WebGPU disabled or unsupported | Enable flags or update the browser |
| Blank canvas | Format mismatch between context and pipeline | Use the same format variable everywhere |
| Flickering triangle | Missing or wrong loadOp | Set loadOp: "clear" |
| Shader compile error | Wrong entry point name or WGSL syntax | Copy the exact names from the shader |
Quick cheat sheet
- Request adapter and device.
- Configure the canvas context.
- Build a minimal pipeline.
- Draw three vertices in a render pass.
Next steps
Once the triangle is stable, the smallest useful upgrades are:
- Resize handling: reconfigure the canvas when the window resizes.
- A uniform buffer: animate the background color or triangle position.
- A vertex buffer: move the triangle geometry out of the shader.
Pick one, add it, verify it, then pick the next. WebGPU makes more sense when each layer is solid.
References
Personal build reflection
I wrote this post after rebuilding the same triangle several times and realizing most failures happened in the same places: adapter availability, canvas format mismatches, and missing clear operations.
The verification step after each stage is the most important part. It turns a fragile copy-paste demo into something you can actually debug.
If you run into a different error, capture the exact console message and which step you were on. That context makes the fix much faster.