VAOs in WebGL 1

Emulating VAOs on WebGL 1 with OES_vertex_array_object

WebGL 1's core has no VAOs; the Khronos-ratified OES_vertex_array_object extension adds them with an OES suffix (createVertexArrayOES(), bindVertexArrayOES() and so on). WebGL 2 promoted it to core, so a WebGL 2 context returns null for it. A thin wrapper gives both the WebGL 2 names:

WebGL 2 VAO names on a WebGL 1 contextJavaScript
const gl1 = document.createElement('canvas').getContext('webgl');
const ext = gl1.getExtension('OES_vertex_array_object');
const vaoApi = gl1 instanceof WebGL2RenderingContext ? gl1 : {
  createVertexArray: () => ext.createVertexArrayOES(),
  bindVertexArray: (vao) => ext.bindVertexArrayOES(vao),
};
const vao = vaoApi.createVertexArray();
vaoApi.bindVertexArray(vao);
gl1.bindBuffer(gl1.ARRAY_BUFFER, gl1.createBuffer());
gl1.enableVertexAttribArray(0);                         // recorded in vao
vaoApi.bindVertexArray(null);
const enabled = () => gl1.getVertexAttrib(0, gl1.VERTEX_ATTRIB_ARRAY_ENABLED);
console.log('outside:', enabled());
vaoApi.bindVertexArray(vao);
console.log('inside:', enabled());
Output
outside: false
inside: true

Without the extension, re-issue the pointer and enable calls before each draw: the state a VAO would restore.

WebGL 1 drawing two meshes through OES_vertex_array_object wrapped in WebGL 2 namesHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  pre { max-width: 600px; margin: 4px 0 0; padding: 6px 8px; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="view" width="600" height="220"></canvas>
<pre id="out"></pre>
<script>
const gl1 = document.getElementById('view').getContext('webgl');      // a WebGL 1 context
const ext = gl1.getExtension('OES_vertex_array_object');
const vaoApi = gl1 instanceof WebGL2RenderingContext ? gl1 : {
  createVertexArray: () => ext.createVertexArrayOES(),
  bindVertexArray: (vao) => ext.bindVertexArrayOES(vao),
};

// GLSL ES 1.00 shaders: attribute/varying, gl_FragColor.
const program = gl1.createProgram();
for (const [type, src] of [[gl1.VERTEX_SHADER, `attribute vec2 aPos; attribute vec3 aColor;
varying vec3 vColor; uniform float uTime;
void main() { vColor = aColor; gl_Position = vec4(aPos.x, aPos.y + 0.04 * sin(uTime + aPos.x * 6.0), 0.0, 1.0); }`],
  [gl1.FRAGMENT_SHADER, `precision mediump float; varying vec3 vColor;
void main() { gl_FragColor = vec4(vColor, 1.0); }`]]) {
  const s = gl1.createShader(type);
  gl1.shaderSource(s, src); gl1.compileShader(s);
  gl1.attachShader(program, s);
}
gl1.bindAttribLocation(program, 0, 'aPos');
gl1.bindAttribLocation(program, 1, 'aColor');
gl1.linkProgram(program);
gl1.useProgram(program);

function mesh(data) {                  // interleaved x, y, r, g, b recorded in a VAO
  const vao = vaoApi.createVertexArray();
  vaoApi.bindVertexArray(vao);
  gl1.bindBuffer(gl1.ARRAY_BUFFER, gl1.createBuffer());
  gl1.bufferData(gl1.ARRAY_BUFFER, new Float32Array(data), gl1.STATIC_DRAW);
  gl1.vertexAttribPointer(0, 2, gl1.FLOAT, false, 20, 0);
  gl1.vertexAttribPointer(1, 3, gl1.FLOAT, false, 20, 8);
  gl1.enableVertexAttribArray(0);      // recorded in the VAO
  gl1.enableVertexAttribArray(1);
  vaoApi.bindVertexArray(null);
  return { vao, count: data.length / 5 };
}
const shelf = mesh([-0.9, -0.75, 0.5, 0.36, 0.24,  0.9, -0.75, 0.5, 0.36, 0.24,  0.9, -0.6, 0.6, 0.45, 0.3,
                    -0.9, -0.75, 0.5, 0.36, 0.24,  0.9, -0.6, 0.6, 0.45, 0.3,   -0.9, -0.6, 0.6, 0.45, 0.3]);
const spines = [];
['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f', '#1f5f8b', '#e09a10'].forEach((hex, i) => {
  const c = [1, 3, 5].map(k => parseInt(hex.slice(k, k + 2), 16) / 255);
  const x0 = -0.85 + i * 0.21, x1 = x0 + 0.17, top = 0.35 + (i % 3) * 0.15;
  for (const [x, y] of [[x0, -0.6], [x1, -0.6], [x1, top], [x0, -0.6], [x1, top], [x0, top]]) spines.push(x, y, ...c);
});
const books = mesh(spines);

// The book's check: attribute 0 is enabled only while the VAO is bound.
const enabled = () => gl1.getVertexAttrib(0, gl1.VERTEX_ATTRIB_ARRAY_ENABLED);
vaoApi.bindVertexArray(null);
const outside = enabled();
vaoApi.bindVertexArray(books.vao);
document.getElementById('out').textContent =
  `${gl1.getParameter(gl1.VERSION)} | extension: ${ext ? 'OES_vertex_array_object' : 'missing'}\n` +
  `VERTEX_ATTRIB_ARRAY_ENABLED outside the VAO: ${outside}, inside: ${enabled()}`;

const uTime = gl1.getUniformLocation(program, 'uTime');
function frame(ms) {
  gl1.clearColor(0.97, 0.96, 0.93, 1);
  gl1.clear(gl1.COLOR_BUFFER_BIT);
  gl1.uniform1f(uTime, 0.0);
  vaoApi.bindVertexArray(shelf.vao); gl1.drawArrays(gl1.TRIANGLES, 0, shelf.count);
  gl1.uniform1f(uTime, ms * 0.003);
  vaoApi.bindVertexArray(books.vao); gl1.drawArrays(gl1.TRIANGLES, 0, books.count);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>