A W3C specification moves from Working Draft to Candidate Recommendation, Proposed Recommendation and finally Recommendation. WebGPU's First Public Working Draft appeared on 18 May 2021, followed by hundreds of drafts. The Candidate Recommendation Snapshot of 19 December 2024 declared the first version feature-complete, and since 16 January 2025 the document has been a continuously updated Candidate Recommendation Draft (CRD), the latest dated 15 September 2026.

For you, CRD means the core API is stable: code written in 2023 still runs, apart from a few early members such as requestAdapterInfo() that were replaced (adapter.info), and new capabilities arrive as optional features (Requesting Optional Features) and WGSL extensions. To advance, the group "expects to demonstrate implementation of each feature in at least two deployed browsers on top of modern GPU system APIs", measured with the conformance test suite in gpuweb/cts (https://github.com/gpuweb/cts 180 ).
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
.stage { position: relative; width: 100%; max-width: 600px; }
.stage canvas { display: block; width: 100%; }
.stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
<canvas id="view" width="600" height="320"></canvas>
<canvas id="labels" width="600" height="320"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');
function showMessage(text) { // 2D fallback when WebGPU is missing
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}
function label(text, x, y, size = 12, color = '#2b2b2b', align = 'center', weight = '') {
ink.font = `${weight} ${size}px system-ui, sans-serif`; ink.fillStyle = color; ink.textAlign = align;
ink.fillText(text, x, y);
}
const stages = ['Working Draft', 'Candidate Rec.', 'Proposed Rec.', 'Recommendation'];
const events = [[2021.38, '18 May 2021', 'First Public WD'], [2024.97, '19 Dec 2024', 'CR Snapshot'],
[2025.04, '16 Jan 2025', 'CR Draft begins'], [2026.71, '15 Sep 2026', 'latest CRD']];
const x = t => 40 + (t - 2021) / 6 * 520;
const DONE = [0.16, 0.56, 0.30], TODO = [0.80, 0.78, 0.74];
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) return showMessage('WebGPU is not available in this browser');
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const boxes = []; // x, y, w, h, r, g, b, corner radius
stages.forEach((name, i) => { // the maturity ladder: WebGPU is at stage 2
const cx = 75 + i * 150, done = i <= 1;
if (i < 3) boxes.push([cx, 58, 150, 6, ...(i < 1 ? DONE : TODO), 3]);
boxes.push([cx - 22, 39, 44, 44, ...(done ? DONE : TODO), 22]);
label(String(i + 1), cx, 67, 16, done ? '#fff' : '#666', 'center', 'bold');
label(name, cx, 104, 13, done ? '#1e6b3a' : '#777');
});
label('WebGPU is here: a continuously updated Candidate Recommendation Draft', 300, 132, 13, '#1e6b3a');
// A year axis with the dated publications
boxes.push([x(2021), 219, x(2027) - x(2021), 3, 0.3, 0.3, 0.3, 1]);
for (let year = 2021; year <= 2027; year++) label(String(year), x(year), 300, 11, '#666');
events.forEach(([t, date, name], i) => {
const up = i % 2 === 0;
boxes.push([x(t) - 1, up ? 180 : 222, 2, 38, 0.85, 0.55, 0.10, 1], [x(t) - 6, 214, 12, 12, 0.85, 0.55, 0.10, 6]);
label(name, x(t), up ? 162 : 274, 12, '#8a5a00');
label(date, x(t), up ? 176 : 288, 11, '#666');
});
const progress = boxes.length; // this box grows every frame
boxes.push([x(2021), 229, 0, 5, ...DONE, 2]);
const module = device.createShaderModule({ code: `
struct Box { rect: vec4f, style: vec4f } // style: r, g, b, corner radius
@group(0) @binding(0) var<storage> boxes: array<Box>;
struct Out { @builtin(position) pos: vec4f, @location(0) local: vec2f,
@location(1) @interpolate(flat) i: u32 }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let corner = vec2f(f32(v & 1), f32(v >> 1)); // triangle-strip corners
let r = boxes[i].rect;
let px = r.xy + corner * r.zw;
return Out(vec4f(px.x / 300 - 1, 1 - px.y / 160, 0, 1), corner * r.zw, i);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
let b = boxes[in.i];
let half = b.rect.zw / 2;
let q = abs(in.local - half) - half + b.style.w; // signed distance to a rounded box
let d = length(max(q, vec2f(0))) + min(max(q.x, q.y), 0) - b.style.w;
let a = clamp(0.5 - d, 0, 1);
return vec4f(b.style.rgb * a, a); // premultiplied alpha
}` });
const blend = { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' };
const pipeline = device.createRenderPipeline({ layout: 'auto',
vertex: { module }, primitive: { topology: 'triangle-strip' },
fragment: { module, targets: [{ format, blend: { color: blend, alpha: blend } }] } });
const data = new Float32Array(boxes.flat());
const buffer = device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
const group = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer } }] });
const start = performance.now();
function frame(now) {
const t = Math.min((now - start) / 2500, 1); // grow to September 2026 in 2.5 s
data[progress * 8 + 2] = (x(2026.71) - x(2021)) * (1 - (1 - t) ** 3);
device.queue.writeBuffer(buffer, 0, data);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
pass.setPipeline(pipeline);
pass.setBindGroup(0, group);
pass.draw(4, boxes.length);
pass.end();
device.queue.submit([encoder.finish()]);
if (t < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
main();
</script>