Using SVG with CSS3 and HTML5 — Supplementary Material

Example code and online extras for the O'Reilly Media book by Amelia Bellamy-Royds, Kurt Cagle, and Dudley Storey.

Borrowing Ready-Made Widgets from HTML

Doing interactivity right isn’t easy on the web. You need to consider every possible way a user could interact with your website: keyboard, mouse, touch, voice control, screen reader, and more. Wherever possible, the easiest approach is to use native HTML input elements, and let the browser developers handle the accessibility.

But SVG doesn’t have native input elements (other than simple links). Does that mean you need to build everything yourself with ARIA attributes and multiple event listeners for different input types?

Not necessarily.

With inline SVG (or even with <foreignObject> in standalone SVG), you can include hidden HTML input elements in the same document as your graphic, and use them to control your SVG. The custom SVG graphics appear to be the interactive widgets, but the hidden HTML elements are controlling the state, managing the keyboard inputs from the user, and presenting the correct values to assistive technology like screen readers and voice control interfaces.

The simplest version of this is the “checkbox hack”, where you create a custom-designed checkbox (or radio button) whose appearance is controlled by the :checked pseudoclass on a clipped sibling HTML input.

Warning

Some older Android browsers (and even recent ones on BlackBerry) do not support dynamic styling with :checked. Other very old browsers do not automatically treat a click on a <label> as a click on its associated element. Depending on your browser support requirements, you may need some browser-sniffing hacks to make sure that the basic HTML input remains visible on these browsers—or on any browsers that don’t support inline SVG.

This code works on at least Android 4.0 and beyond, and Internet Explorer 9+.

In its purest form, the checkbox hack is used to make prettier checkboxes and radio buttons. You hide the default input, and add your own (inline SVG) checkbox to the input’s <label>.

The basic markup might look something like this:

<input type="checkbox"
       class="hidden-check" id="confirmation" />
<label for="confirmation">
    <svg aria-hidden="true">
        <use xlink:href="#checkbox-outline" />
        <use class="visible-check"
             xlink:href="#checkbox-checkmark" />
    </svg>
    <span class="label-text">
        <strong>Yes,</strong> I agree
        to the Terms &amp; Conditions
    </span>
</label>

The SVG is aria-hidden because it has no meaning of its own: it only duplicates the meaning of the hidden checkbox.

The relevant CSS hides the real checkbox, and then styles the SVG checkbox according to the :checked state:

.hidden-check {
    position: absolute;
    clip: rect(0,0, 1px,1px);
}
.hidden-check + label .visible-check {
    opacity: 0;
    transition: opacity 0.3s;
}
.hidden-check:checked + label .visible-check {
    opacity: 1;
}

When the checkbox is selected—by keyboard activation or by clicking or tapping on the label—the SVG checkmark will fade into view. Since the SVG itself is part of the <label>, clicking or tapping on the visible checkbox will have the same effect, toggling the checkmark on and off.

The <input> checkbox itself is hidden—while still being interactive—using absolute positioning and the clip property. Visually hiding an interactive element is the only recommended use for this otherwise deprecated property. (Alternatively, you can use opacity: 0.01. For maximum screen-reader compatibility, don’t make it completely transparent or offscreen.)

Example 18-X1 gives a complete set of styles and SVG markup, including :focus styles. Figure 18-X1 shows what our checkbox looks like when it is both focused and checked—or more precisely, it shows what the custom label looks like when the hidden real checkbox is both focused and checked. View the live example in your browser.

The words 'Yes, I agree to the Terms and Conditions' beside a checkbox drawn as a square with inwardly-curved sides, and a large green checkmark overtop and extending from the outline.  The checkbox is surrounded by a dotted blue focus outline.
Figure 18-X1. A custom SVG checkbox in an HTML form
Example 18-X1. Creating a custom checkbox with hidden HTML, inline SVG, and CSS pseudoclasses
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Custom Accessible SVG Checkbox</title>
    <style>
body {
    font-size: 250%;
    font-family: sans-serif;
    color: darkOrchid;
}

.hidden-check {
    position: absolute;
    clip: rect(0,0, 1px,1px);
}
label > svg, label > .label-text {
    display: inline-block;
    vertical-align: middle;
}
label > svg {
    width: 1em;
    height: 1em;
    margin: 0 0.5em;
    overflow: visible;
}
label > .label-text {
    max-width: calc(100% - 3em);
}
.hidden-check + label .visible-check {
    opacity: 0;
    transition: opacity 0.3s;
}
.hidden-check:checked + label .visible-check {
    opacity: 1;
}
.hidden-check:focus {
    outline: none;
}
.hidden-check:focus + label > svg {
    outline: dotted deepSkyBlue;
    outline-offset: 0.1em;
}
    </style>
</head>
<body>
    <svg class="defs-only" width="0" height="0"
         aria-hidden="true" focusable="false">
        <symbol id="checkbox-outline" viewBox="0 0 10 10"
                overflow="visible">
            <path stroke="currentColor" fill="none"
                  d="M1,1 Q5,2 9,1 Q8,5 9,9
                          Q5,8 1,9 Q2,5 1,1Z" />
        </symbol>
        <symbol id="checkbox-checkmark" viewBox="0 0 10 10"
                overflow="visible">
            <path stroke="seaGreen" fill="none"
                  stroke-width="2px" stroke-linecap="round"
                  filter="drop-shadow(0.5px 0.5px 0.3px
                            rgba(0,0,0,0.5))"
                  d="M2,5 L5,8 Q6,4 9,2" />
        </symbol>
    </svg>
    <form>
        <!-- The rest of the form goes here -->
        <input class="hidden-check" type="checkbox"
               id="confirmation"/>
        <label for="confirmation">
            <svg aria-hidden="true">
                <use xlink:href="#checkbox-outline" />
                <use class="visible-check"
                     xlink:href="#checkbox-checkmark" />
            </svg>
            <span class="label-text">
                <strong>Yes,</strong> I agree
                to the Terms &amp; Conditions
            </span>
        </label>
        <!-- Submit button goes here -->
    </form>
</body>
</html>

The same approach can be used to create larger interface switches, separate from a form input. For example, we could use HTML radio buttons to finally create the optimal interactive semantics for our stroke-linejoin demo (which was last updated in Example 18-7).

To recap:

Example 18-X2 converts our all-SVG demo into an HTML web page with inline SVG, controlled by HTML radio-button <input> elements.

The SVG <text> labels have been replaced by HTML <label> elements below the inline SVG. Each label element is matched to the corresponding <input> with the for attribute, which references the input’s id (without a # URL hash).

Figure 18-X2 shows the web page in action. View the live example in your browser.

In the middle of the figure, the same blue-and-purple cross shape as the previous demos. The corners of the strokes are trimmed with a bevel snipped-corner effect.  Above, the text instruction (Selecting one of the stroke-linejoin options will apply that effect to the sample shape).  Below, the words round, bevel, and miter. Bevel is written in a darker color and has an underline.  There is a dark outline rectangle around all three text labels.
Figure 18-X2. Inline SVG controlled by hidden HTML radio buttons
Example 18-X2. Using HTML radio buttons and :checked to switch SVG style options
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>HTML radio-buttons to control SVG styles</title>
    <style>
        figure {
            max-width: 100vh;
            padding: 0;
            margin: 1em auto;
        }
        figcaption {
            text-align: center;
            font-style: italic;
        }
        svg {
            display: block;
            margin: 1em 0;
        }
        .shape {
            fill: deepSkyBlue;
            stroke: blueViolet;
            stroke-width: 4px;
            stroke-opacity: 0.8;
        }
        .option {
            position: absolute;
            clip: rect(0,0, 1px,1px);
        }
        .option-set {
            display: flex;
            text-align: center;
            justify-content: space-around;
            padding: 1em;
        }
        .option-label {
            display: inline-block; /* fallback for no flexbox */
            flex-basis: 8em;
            color: blueViolet;
            font: 20px Consolas, monospace;
        }
        #round:checked ~ svg .shape { stroke-linejoin: round; }
        #bevel:checked ~ svg .shape { stroke-linejoin: bevel; }
        #miter:checked ~ svg .shape { stroke-linejoin: miter; }

        #round:checked ~ .option-set .option-label[for="round"],
        #bevel:checked ~ .option-set .option-label[for="bevel"],
        #miter:checked ~ .option-set .option-label[for="miter"] {
            color: indigo;
            text-decoration: underline;
        }
        #round:focus ~ .option-set .option-label[for="round"],
        #bevel:focus ~ .option-set .option-label[for="bevel"],
        #miter:focus ~ .option-set .option-label[for="miter"],
        .option-label:hover {
            outline: none;
            text-decoration: underline;
            text-decoration-color: indigo;
        }
        .option:focus { outline: none; }
        .option:focus ~ .option-set { outline: solid indigo 2px; }
    </style>
</head>
<body>
<figure>
    <figcaption>
        Selecting one of the stroke-linejoin options
        will apply that effect to the sample shape.
    </figcaption>
    <input id="round" class="option" type="radio" name="linejoin"/>
    <input id="bevel" class="option" type="radio" name="linejoin"/>
    <input id="miter" class="option" type="radio" name="linejoin"/>
    <svg viewBox="-18 -12 36 24">
        <path class="shape"
              d="M-5,-10 H5  V-5 H10  L15,0  L10,5   H5  V10
                         H-5 V5  H-10 L-15,0 L-10,-5 H-5 V-10 Z" />
    </svg>
    <div class="option-set">
        <label for="round" class="option-label">round</label>
        <label for="bevel" class="option-label">bevel</label>
        <label for="miter" class="option-label">miter</label>
    </div>
</figure>
</body>
</html>
Tip

Although we’ve added distinct :focus versus :checked styles for the labels, most browsers automatically switch a radio button selection when you arrow through the choices. But they are conceptually different states, so it’s good to keep them separate.

Because the current selection is now maintained by the HTML radio button group, not by the target fragment in the URL, you could add a second radio group controlling a separate property (stroke-linecap, maybe?) and an independent set of labels.

It’s a lot of CSS code, and it’s undeniably a hack of the <input> element’s intended purpose, but it works.

The HTML <label> elements automatically transfer mouse clicks to the hidden radio buttons, so the demo works for mouse and touch users as well as keyboard users.

If we wanted to keep the visible labels as SVG text, we would have needed JavaScript to respond to clicks and update the HTML inputs accordingly. We would have also needed aria-labelledby to connect the <input> elements to their SVG labels, since the for attribute only works on <label>.

JavaScript is also required for more complex inputs (like range sliders), whose current values aren’t accessible through CSS pseudoclasses. But instead of having to code all the keyboard listeners and accessibility behaviors for a fully-functioning slider, you can just listen for change events on the hidden HTML <input type="range"> and update your SVG accordingly. Just remember to also update the input’s value to match whatever click-and-drag behavior you’re using for mouse and touch users.

The same concept could even be extended to text boxes. You’ll need to listen for focus changes and keypress events on the hidden text box, and then retrieve the current text content and selection range, and use that to update your SVG text representation of the input. You’ll also need to listen for selection events on the SVG text itself—so that mouse users can select the text they want to change—and transfer those selections and the keyboard focus to the hidden input.

Which is a lot of code, but still less than coding a fully-functional text input from scratch.

As we mentioned in Chapter 15, the HTML contenteditable attribute can also sort-of apply to inline SVG, by applying it to a parent HTML element. But you’ll still need to add a bit of JavaScript, to prevent the focus from dropping out of your SVG text elements if the user deletes the text completely. There is currently no support (in the spec or in most browsers) for contenteditable on individual SVG elements. Hopefully, it will be added in the future.