HTML5 Canvas and the Canvas Shadow DOM

The canvas shadow DOM enables you to provide code behind a canvas element for better user interaction and accessibility.

What is the Canvas Shadow DOM?

HTML5 Canvas provides a rich drawing surface that can be used for graphics, images, and even video display that can be manipulated with a collection of properties and methods. However, canvas is a strictly visual medium, which makes it difficult for screen readers and assistive technology to understand, without a little help.

The canvas shadow DOM is a sub version of the Document Object Model (DOM) in Windows Internet Explorer that offers a way to create an alternate interactive experience for assistive technology, as well as for browsers that don’t support Canvas. When you use the canvas tag, it is common to put some fallback code between the two canvas tags for browsers that lack canvas support. For example:

<html>
<head>
    <title>Canvas not supported example</title>
</head>
<body>
  <canvas id="coolstuff" height="400" width="500" >
    HTML5 canvas support is required for a fully interactive experience, so here’s this: <br/>
    <img src=”staticimage.png” alt="some cool stuff"/>
  </canvas>
</body>
</html>

In a browser that supports canvas, the user might see a JavaScript animated image. If canvas isn’t supported, the canvas tag is ignored and the static image is displayed. Through this technique, the canvas shadow DOM can do its magic.

Fallback content - is it there, or not?

The fallback content, in this case a message and image, are normally hidden if canvas is supported in the browser. However, the content is still alive and well, and can be read and interacted with by assistive technology devices, such as screen readers.

Unlike a distant cousin, the alt attribute, the code between the <canvas> </canvas> tags is its own sub-DOM tree for to assistive technology. In its simplest form, you can add text to replace the graphic. The text can include HTML structure, such as lists and tables, which are not available with the alt attribute. The next example puts an ordered list behind a canvas.

<html>
<head>
    <title>Canvas fall back example</title>
</head>
<body>
<canvas id="coolstuff" height="400" width="500">
   <div>The image here shows the future highway that will be: 
     <ol>
        <li>Connecting the east side of the mountain to the west side</li>
        <li>Providing four wide lanes for traffic</li>
        <li>Adding a bike lane</li>
     </ol>
   </div>
</canvas>
</body>
</html>

You can also add fully interactive code, so if something changes in the canvas element, it can be reflected in the canvas DOM. Keeping the two in sync however, is the developer’s responsibility, but if done right, can be very powerful. The following is a simple example of using the same data and mirroring it either in a canvas based fan control, or a list. Most of the JavaScript code in the example is building a graphic fan control. Between the canvas tags is a simple list that displays the same information.

<html>
<head>
<title>Canvas and ARIA fan speed control</title>

<script type="text/javascript">

// Drawing & UI handler code should get the state from the actual controls
// or, the author has to do some work to sync controls & variables
function GetSpeed() {
    if (document.getElementById("r0").getAttribute("aria-checked") == "true") return 0;
    if (document.getElementById("r1").getAttribute("aria-checked") == "true") return 1;
    if (document.getElementById("r2").getAttribute("aria-checked") == "true") return 2;
    if (document.getElementById("r3").getAttribute("aria-checked") == "true") return 3;
}

var scaled = false;

// Draw the custom control
function Draw(focus) {
    var canvas = document.getElementById("canvaselement");
    if (canvas.getContext) {

        var fanspeed = GetSpeed();
        var ctx = canvas.getContext('2d');

        ctx.clearRect(0, 0, 100, 100);

        if (scaled == false) {
            ctx.scale(3, 3);
            scaled = true;
        }

        // Draw control
        ctx.beginPath();
        ctx.strokeStyle = "rgb(0, 0, 0)";
        ctx.arc(50, 50, 20, 0, 2 * Math.PI, false);
        ctx.stroke();

        // Draw control text
        ctx.fillText("Fan speed", 2, 12);
        ctx.fillText("Off", 43, 80);
        ctx.fillText("Low", 8, 53);
        ctx.fillText("Medium", 32, 26);
        ctx.fillText("High", 74, 53);

        //Draw selected setting
        var x;
        var y;
        switch (fanspeed) {
            case 0:
                x = 50;
                y = 62;
                break;
            case 1:
                x = 38;
                y = 50;
                break;
            case 2:
                x = 50;
                y = 38;
                break;
            case 3:
                x = 62;
                y = 50;
                break;
        }
        ctx.beginPath();
        ctx.strokeStyle = "rgb(255, 0, 0)";
        ctx.arc(x, y, 4, 0, 2 * Math.PI, false);
        ctx.stroke();

    }
    }

    // The UI handler for the custom control
    function Cycle(direction) {
        var fanspeed = GetSpeed();
        var o = "r" + fanspeed; //old radio button to clear
        var ox = document.getElementById(o);
        ox.setAttribute("aria-checked", "false");
        ox.setAttribute("tabindex", "-1");

        if (direction == "down") {
            fanspeed = fanspeed - 1;
            if (fanspeed == -1) fanspeed = 3;
        }
        else {
            fanspeed = fanspeed + 1;
            if (fanspeed == 4) fanspeed = 0;
        }

        var r = "r" + fanspeed; //radio button to set
        var rx = document.getElementById(r)
        rx.setAttribute("aria-checked", "true");
        rx.setAttribute("tabindex", "0");
        rx.focus();

        Draw(true);
    }

    function Keys(event) {
        var key = event.keyCode;
        if (key == 37 || key == 40) {
            Cycle("down");
        }
        else if (key == 38 || key == 39 || key == 32 || key == 13) {
            Cycle("up");
        }
        //else do nothing
    }

</script>



</head>
<body>


<canvas id="canvaselement" width="400" height="400" onclick="Cycle('up');">
<div>Fan speed</div>

    <div id="fancontrolelement" role="radiogroup" onkeydown="Keys(event);">
        <div id="r0" role="radio" aria-checked="true" tabindex="0" class="radio">Fan Speed: Off</div>
        <div id="r1" role="radio" aria-checked="false" tabindex="-1" class="radio">Fan Speed: Low</div>
        <div id="r2" role="radio" aria-checked="false" tabindex="-1" class="radio">Fan Speed: Medium</div>
        <div id="r3" role="radio" aria-checked="false" tabindex="-1" class="radio">Fan Speed: High</div>
    </div>

</canvas>

<!-- Do the initial draw of the element -->
<script type="text/javascript">Draw(false)</script>

</body>
</html>

In this example when canvas isn’t supported it shows a table that is driven by the same data that drives the canvas fan control. In a screen reader, even if canvas is supported, the table can be read. To test this yourself, open F12 developer tools, and change the Document Mode to something other than Windows Internet Explorer 9 or Internet Explorer 10.

A browser that supports canvas will see this graphic:

Pie chart graphics of results

A browser that doesn't support canvas, or a screen reader will "see" this:

Table of results

Canvas Shadow DOM isn't just for accessibility

The canvas shadow DOM can also provide better interactivity. For example, if you render a stock chart from data in a table contained in the canvas DOM, you can use the keyboard to make updates. Using forms and fields, such as radio buttons, your interaction from the keyboard can be reflected back through the canvas graphic for sighted users with supported browsers, and the form and buttons otherwise.

Because the canvas shadow DOM can contain any HTML elements, you can store and access interactive data based on the state of elements, such as buttons and input tags. Even though elements might be hidden from view to a sighted person when canvas is supported, JavaScript can still access the elements.

Canvas graphics and the canvas shadow DOM are good for single controls, and can provide a great experience for the user and developer alike. However, the app can only get focus and a bounding rectangle for the whole canvas, not sections of it. This means that keyboard focus goes to a point at canvas coordinates 0,0. This will affect anything that depends on the focus, like screen magnifiers. If your app requires more complex interactive graphic experiences, you might want to look at SVG and ARIA markup for accessibility.