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.

Rendering pipeline checkpoint illustration for this section.

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 serve or 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 = `
@vertex
fn 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);
}
@fragment
fn 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:

  1. The triangle is visible.
  2. The console has no errors.
  3. 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

SymptomLikely causeFix
navigator.gpu is undefinedWebGPU disabled or unsupportedEnable flags or update the browser
Blank canvasFormat mismatch between context and pipelineUse the same format variable everywhere
Flickering triangleMissing or wrong loadOpSet loadOp: "clear"
Shader compile errorWrong entry point name or WGSL syntaxCopy the exact names from the shader

Quick cheat sheet

  1. Request adapter and device.
  2. Configure the canvas context.
  3. Build a minimal pipeline.
  4. Draw three vertices in a render pass.
Graphics systems checkpoint illustration for this section.

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.