Learn Creative Coding (#138) - Accessibility in Creative Coding

Last time I left you with a promise that's been quietly building for a few episodes now: we're at the point where our work stops being something only we run and becomes something other people rely on. We gave your art a memory with Git, we gave it a conscience with tests, and both of those were really about making code you can trust when someone else is depending on it. Well - today that "someone else" gets a face. Because the very first question the moment your art leaves your own screen is not "does it run on their machine", it's something more basic and, honestly, more human: can they even experience it at all? Allez, that's what this whole episode is about :-).
I'll be straight with you, this is the topic I ignored the longest. For years I thought accessibility was a checklist for corporate websites - alt text and form labels and grey compliance stuff that had nothing to do with making beautiful pictures. And then I put a piece up at a small local show, all sound-reactive swirling motion, dead proud of it, and a woman told me she'd had to look away because that kind of movement makes her physically nauseous. She wanted to see it. My art literally pushed her out of the room. That stung more than any bug ever has, and it's when the coin dropped for me: accessibility isn't a compliance chore, it's just the question of who gets to be in the room with your work. Let me show you what I figured out since.
Motion: the one that pushed someone out of my show
Let's start with the exact thing that bit me, because it's the most common way creative code hurts people and the easiest to fix. A meaningful slice of the population has vestibular sensitivity - big swirling motion, parallax, fast zooms, screen-filling drift can make them dizzy, nauseous, or trigger a migraine. This is not "some people find it a bit much". It's a genuine physical reaction, the same family as motion sickness in a car.
And here's the beautiful part: your reader has already told their computer they're sensitive to this. Every OS has a "reduce motion" accessibility setting, and the browser hands it to you for free through a media query. You just have to ask.
// the single most important accessibility line in creative coding.
// the OS-level "reduce motion" preference, handed straight to you.
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduceMotion) {
console.log("this reader asked for less motion - respect it");
}
That one boolean is a person telling you, in advance, "please go gentle on me". So now the job is to honour it. And notice what I'm NOT saying: I'm not saying rip all the motion out and show everyone a boring still frame. That would punish the 95% who are fine with movement to protect the 5% who aren't. The trick is to branch - a lively version for people who want it, a calm version for people who need it.
// branch the whole sketch on the preference. two experiences, one codebase.
function makeSketch() {
if (reduceMotion) {
// the CALM path: land on one gorgeous still composition, no drift.
drawStillComposition(42); // a single seeded frame (episode 24!) - no loop
} else {
// the LIVELY path: the full animated thing, motion and all.
startAnimationLoop();
}
}
See the mindset there? The reduced-motion version isn't a punishment, it's a different good experience - one frozen frame of the same generative system, picked to be beautiful on its own. Your art still shows up. It just shows up in a form that doesn't make someone sick. And because we seed our work (that reproducible randomness from episode 24), landing on one lovely still frame is trivial - you already know how to pick and freeze a single composition.
One more layer, for the folks who want some motion but not a firehose of it: give them a middle gear.
// three tiers, not two. let the intensity itself scale to the preference.
function motionScale() {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return 0; // still
return 1; // full
}
function update(particle) {
const gain = motionScale();
particle.x += particle.vx * gain; // gain 0 = frozen, gain 1 = full life
particle.y += particle.vy * gain;
}
Multiplying your motion by a single gain value means one variable controls the whole feel of the piece, from dead-still to fully alive. That's a pattern worth keeping in your back pocket for the settings panel we'll build later.
Colour: not everyone sees the palette you see
Right, next big one, and it lands right on top of episode 7 where we did colour theory. Roughly one in twelve men and one in two hundred women have some form of colour vision deficiency - most commonly trouble telling reds from greens apart. Think about how much creative code leans on colour to carry meaning: "the red particles are the fast ones", "the green regions are safe". For a big chunk of your audience, red and green can look like the same muddy colour. Your carefully chosen palette can quietly collapse into mush.
Before we fix it, it helps to see it. We can simulate what a colour looks like to someone with the most common deficiency (deuteranopia) by pushing the RGB through a known transform. It's the same "map one set of values onto another" move we did all through the data episodes.
// simulate deuteranopia (red-green colour blindness) on an [r,g,b] colour.
// rough but useful - lets you PREVIEW your palette the way many readers see it.
function simulateDeuteranopia(r, g, b) {
// project onto the colour plane a deuteranope actually perceives
const R = 0.625 * r + 0.375 * g + 0.0 * b;
const G = 0.700 * r + 0.300 * g + 0.0 * b;
const B = 0.0 * r + 0.300 * g + 0.700 * b;
return [Math.round(R), Math.round(G), Math.round(B)];
}
// run your palette through it and eyeball the result:
const palette = [[220, 50, 50], [50, 180, 50]]; // a red and a green
palette.forEach(c => console.log(c, "->", simulateDeuteranopia(...c)));
// the red and green come out alarmingly close. that's the problem, visualised.
The first time you run your own palette through that and watch your bold red and your bold green come out as two nearly identical browns, it's a proper little wake-up. So what do we do about it? The single most powerful rule in the whole of accessible visuals is this: never let colour be the only thing carrying the meaning. Back it up with a second channel - shape, position, size, a label, a texture. If two things need to be told apart, make them differ in more than hue.
// BAD: colour is the only difference. invisible to a colour-blind reader.
function drawTokenBad(ctx, x, y, isSpecial) {
ctx.fillStyle = isSpecial ? "red" : "green";
ctx.beginPath(); ctx.arc(x, y, 10, 0, Math.PI * 2); ctx.fill();
}
// GOOD: colour AND shape both carry it. now it survives ANY colour vision.
function drawTokenGood(ctx, x, y, isSpecial) {
ctx.fillStyle = isSpecial ? "#d94" : "#49d"; // still coloured, still pretty
if (isSpecial) {
// a triangle for special...
ctx.beginPath();
ctx.moveTo(x, y - 11); ctx.lineTo(x + 10, y + 8); ctx.lineTo(x - 10, y + 8);
ctx.closePath(); ctx.fill();
} else {
// ...a circle for normal. the SHAPE tells the story even in greyscale.
ctx.beginPath(); ctx.arc(x, y, 10, 0, Math.PI * 2); ctx.fill();
}
}
Squint at the "good" version, or imagine it printed in black and white - you can still tell the two apart, because the difference lives in the geometry, not just the colour. That's the test I use now: would this still read if you printed it on a grey photocopier? If yes, you're safe for basically every kind of colour vision.
And when you do pick colours, lean on palettes that were designed to stay distinct under colour blindness. You don't have to invent these - people far cleverer than me already did the work.
// colour-blind-safe qualitative palettes (Okabe-Ito style).
// these stay distinguishable under the common deficiencies - use them freely.
const SAFE = [
"#e69f00", // orange
"#56b4e9", // sky blue
"#009e73", // bluish green
"#f0e442", // yellow
"#0072b2", // blue
"#d55e00", // vermillion
"#cc79a7", // reddish purple
];
function categoryColour(i) {
return SAFE[i % SAFE.length]; // cycle through the safe set for your categories
}
That palette isn't just "accessible", it's genuinly gorgeous - warm and clean and it photographs beautifully. Accessible and pretty were never enemies. Wich is the theme of this whole episode, really.
Contrast: can they even see it against the background?
One more colour thing, quick but important. Even with perfect colour vision, thin light-grey text on a white ground is a strain to read, and for low-vision folks it can be invisible. There's an actual measurable standard for this - the WCAG contrast ratio - and you can compute it in code, so you never have to guess whether your label is readable.
// relative luminance of an sRGB colour (0..1), per the WCAG formula.
function luminance(r, g, b) {
const a = [r, g, b].map(v => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
}
// contrast ratio between two colours: 1 (none) up to 21 (black on white).
function contrastRatio(rgb1, rgb2) {
const L1 = luminance(...rgb1);
const L2 = luminance(...rgb2);
const [hi, lo] = L1 > L2 ? [L1, L2] : [L2, L1];
return (hi + 0.05) / (lo + 0.05);
}
console.log(contrastRatio([120, 120, 120], [255, 255, 255])); // ~2.8 - too weak for text
console.log(contrastRatio([40, 40, 40], [255, 255, 255])); // ~11 - lovely and legible
The rule of thumb worth memorising: normal text wants a ratio of at least 4.5, big/bold text can get away with 3. Any overlaid caption, title, or UI label on your artwork should clear that bar. I keep this function in my toolkit and just assert on it (remember the assertions from last episode?) so a caption can never silently drift into unreadable-grey.
Keyboard: not everyone has a mouse
Now let's leave colour and talk interaction, because this is where creative coders trip constantly. So many of our interactive pieces are built entirely around the mouse - move the cursor, the flow field bends; click, particles spawn. But plenty of people can't use a mouse at all. Motor disabilities, people navigating entirely by keyboard, folks on assistive switches - if the only way into your piece is mouseX/mouseY, you've locked them out completely.
The fix is not to throw away the mouse - it's to make sure every mouse interaction has a keyboard twin that does the same job.
// mouse drives a focal point. GOOD - but if it's the ONLY way in, it excludes people.
let focus = { x: 200, y: 200 };
canvas.addEventListener("mousemove", e => {
focus.x = e.offsetX;
focus.y = e.offsetY;
});
// so give that exact same focal point a KEYBOARD twin. arrows move it too.
window.addEventListener("keydown", e => {
const step = 15;
if (e.key === "ArrowLeft") focus.x -= step;
if (e.key === "ArrowRight") focus.x += step;
if (e.key === "ArrowUp") focus.y -= step;
if (e.key === "ArrowDown") focus.y += step;
if (e.key === " ") spawnBurst(focus.x, focus.y); // space = the "click"
});
Notice both paths write to the same focus object - the sketch itself doesn't know or care whether a mouse or the arrow keys moved it. That's the clean way to do it: one shared state, many ways to poke it. A keyboard user gets the whole experience, just driven differently. And there's a bonus - keyboard control makes your piece recordable and scriptable too, which is handy for exactly the kind of automated snapshot tests we built last episode.
For a keyboard user to reach your canvas at all, though, it has to be focusable and it has to show when it's focused - otherwise they're pressing arrow keys into the void with no idea it's listening.
// make the canvas reachable by Tab, and visibly show when it has focus.
canvas.setAttribute("tabindex", "0"); // 0 = joins the normal Tab order
canvas.addEventListener("focus", () => {
canvas.style.outline = "3px solid #0072b2"; // a clear "you're here" ring
});
canvas.addEventListener("blur", () => {
canvas.style.outline = "none";
});
That tabindex="0" is a tiny line that does a huge amount - it drops your canvas into the page's keyboard tour so a Tab-key user actually lands on it. Never, ever remove a focus outline without replacing it with something equally visible. That glowing ring is a keyboard user's cursor. Taking it away is like hiding their mouse pointer.
Screen readers: the canvas is a black hole
Okay, the hardest one, and the one almost nobody handles - screen readers. A blind or low-vision reader navigates the web through software that reads the page aloud. And here's the brutal truth about our medium: a <canvas> element is, to a screen reader, a completely opaque black box. All your gorgeous pixels? It sees nothing. Literally an empty rectangle with no information at all. All that work, and for a screen-reader user it's a blank spot on the page they'll just skip past.
We can't make them see the pixels. But we can give them a description - a text alternative that conveys what the piece is and what it's doing. The first, simplest move is a plain label on the canvas itself.
// give the black box a voice: a text description a screen reader CAN read.
canvas.setAttribute("role", "img");
canvas.setAttribute(
"aria-label",
"Generative flow field: hundreds of warm-orange particles drifting like ink in water across a dark canvas."
);
That one attribute is the difference between "unlabelled graphic, skip" and an actual sentence describing your art, spoken aloud. It costs you thirty seconds and it's the single highest-value accessibility thing you can do for a static piece. Write it like you'd describe the piece to a friend over the phone - what's the mood, the colour, the movement, the feeling.
For a piece that changes, a static label isn't enough - the description goes stale the moment the art evolves. That's where an ARIA live region comes in: a hidden bit of text that, when you update it, the screen reader announces automatically. It's how you narrate a generative piece as it unfolds.
// a hidden, screen-reader-only region that ANNOUNCES changes as they happen.
const live = document.createElement("div");
live.setAttribute("aria-live", "polite"); // "polite" = wait for a gap, don't interrupt
live.style.position = "absolute";
live.style.width = "1px"; live.style.height = "1px";
live.style.overflow = "hidden"; live.style.clip = "rect(0 0 0 0)"; // visually hidden, still read
document.body.appendChild(live);
// then narrate meaningful moments - NOT every frame (that'd be a nightmare of chatter):
function announce(message) {
live.textContent = message; // updating the text triggers the screen reader
}
announce("The composition has settled. 200 particles have formed three slow spirals.");
The art of this is restraint - you narrate the meaningful beats ("a new pattern emerged", "the piece reached its resting state"), not every twitch of every frame, or you'd drown the listener in babble. Think of it like commentating: you describe the moments that matter, not each individual pixel. It takes taste, the same taste you use to decide which git commits are worth a message.
And for a genuinly generative system, you can even build the description from the state, so it's always accurate - the same "map data to a human-readable property" idea from episode 82, just mapping to words instead of visuals.
// turn the sketch's live state into an honest sentence. describe, don't decorate.
function describe(state) {
const density = state.particles.length > 300 ? "densely packed" : "sparse and calm";
const mood = state.palette === "warm-ink" ? "warm oranges and reds" : "cool blues";
return `A ${density} field of ${state.particles.length} particles in ${mood}, `
+ `slowly drifting ${state.windDir}.`;
}
announce(describe(currentState)); // always truthful, because it's generated FROM the art
Generating the words straight from the state means the description can never lie - it's derived from the same numbers that draw the picture. That's a lovely little property, and it's only possible because we've been disciplined about keeping our sketch's state in one tidy place all series long.
Bringing it together: an accessibility layer
Right, that's a lot of separate pieces - motion, colour, contrast, keyboard, screen reader. Let me show you how I actually keep them tidy in a real project, because five scattered accessibility hacks is a maintenance headache. I bundle the reader's preferences into one small object that the whole sketch consults - the same "one settings object drives everything" pattern we built the GUI around back in episode 133.
// one place that knows all the reader's needs. the sketch asks IT, not the DOM.
const a11y = {
reduceMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches,
safePalette: true, // default to colour-blind-safe colours
announce(msg) { // the live-region narrator from above
live.textContent = msg;
},
motionGain() {
return this.reduceMotion ? 0 : 1;
},
colour(i) {
return this.safePalette ? SAFE[i % SAFE.length] : RAW[i % RAW.length];
},
};
// now the sketch just consults a11y and everything stays consistent:
function update(p) { p.x += p.vx * a11y.motionGain(); }
function colourOf(i) { return a11y.colour(i); }
Everything routes through that one object, so accessibility stops being a pile of special cases sprinkled through your code and becomes a clean, single source of truth you can even expose as toggles in a settings panel. A reader who wants the full motion back can flip reduceMotion off; one who finds the safe palette a bit flat can try the raw one. You're not deciding for them - you're giving them the controls and sensible defaults. That, to me, is the whole philosophy in one object: accessibility done right isn't taking choices away from anyone, it's offering more of them.
But does this ruin the art?
I know some of you have been quietly worried this whole episode, because I was too, once. Doesn't all this water the work down? Don't I have to make everything tamer, blander, safer, until it's lost all its edge? And the honest answer I've landed on after years of this: no, almost never - and when there's genuinly a tension, you resolve it by offering versions, not by amputating.
Think about what we actually did. Reduced motion didn't kill the animation, it added a beautiful still variant. Safe palettes didn't make things ugly, they're gorgeous and they photograph better. Shape-plus-colour didn't dumb down the composition, it made it stronger, readable even in greyscale. A screen-reader description didn't touch the pixels at all, it just let a whole group of people finally know the piece exists. Every single one of those made the work reach further without making it lesser. The constraint, like most good constraints in art, pushed me somewhere better than I'd have gone unconstrained. Allez, that's the secret nobody tells you - accessibility is a creative brief, not a cage.
Where this is heading
Here's the thread pulling us forward. This whole late stretch of the series is about your work leaving your own screen and living in the wider world - and we've just made sure that when it lands in front of anyone, they can actually get in. But there's a piece still missing. Right now, all this care lives in your head and your code, and the moment someone else picks up your sketch - to run it, to build on it, to exhibit it - they have to reverse-engineer everything you knew. That's the gap we close next: writing your work down so it survives being handed over, so the knowledge doesn't evaporate the second you walk away from it. And a little further out, once your art is reliable and accessible and documented, you're finally ready to build things other people don't just look at but actually use - which is where these final episodes are quietly headed. So this week: take one interactive sketch you've made and give it three gifts - honour prefers-reduced-motion, add arrow-key control alongside the mouse, and write one honest aria-label sentence describing it. Then open it and Tab your way in with your hands off the mouse entirely. Feel how it feels to reach your own art the way someone else might have to. That little shift in perspective is the whole point :-).
't Komt erop neer...
- Accessibility isn't a compliance chore, it's the question of who gets to be in the room with your work. The moment your art leaves your screen, the first question isn't "does it run" but "can they even experience it"
- Motion can physically hurt people - vestibular sensitivity is real. Read
prefers-reduced-motionand branch: a lively version for those who want it, a calm still frame (episode 24's seeding makes this trivial) for those who need it. A singlemotionGainof 0 or 1 can scale the whole feel - Colour is not universal - around 1 in 12 men have colour vision deficiency. Simulate your palette to see the problem, then follow the golden rule: never let colour be the only thing carrying meaning. Back it with shape, position or label so it survives a grey photocopier
- Lean on colour-blind-safe palettes (Okabe-Ito style) - they stay distinct under the common deficiencies and they're genuinly beautiful. Accessible and pretty were never enemies
- Check your contrast with the WCAG ratio in code - 4.5 for normal text, 3 for big text. Never guess whether a caption is readable, measure it
- Not everyone has a mouse. Give every mouse interaction a keyboard twin writing to the same shared state, make the canvas
tabindex="0"so Tab reaches it, and always show a visible focus ring - that ring is a keyboard user's cursor - A canvas is a black hole to screen readers - it sees zero pixels. Give it a voice:
role="img"plus a writtenaria-label, and for evolving pieces an ARIA live region that narrates the meaningful beats (with restraint, not every frame). Generate the description from the state (episode 82!) so it can never lie - Bundle it all into one
a11yobject the sketch consults - the same single-source-of-truth idea as the settings panel from episode 133. Accessibility stops being scattered hacks and becomes clean, toggleable defaults - It does not ruin the art. When there's real tension, you resolve it by offering versions, not amputating. Every accessible choice here made the work reach further without making it lesser - the constraint is a creative brief, not a cage
So that's the answer to the itch from last time - "other people rely on your work" starts with the most basic thing of all: making sure they can actually get in the door. Honour their motion setting, don't hide meaning in hue alone, let them in with a keyboard, and give the black box a voice. None of it dims the work, and all of it widens the room. Do those three small gifts to one sketch this week, then Tab into your own art with your hands off the mouse, and I promise you'll never build a locked door again. Merci for reading, and go make some room :-).
Sallukes! Thanks for reading.
X