Custom Controls

Status: This is an in-progress, unapproved draft.

Sometimes form designs require something beyond what is achievable with a standard form control. In such cases it is possible to build added functionality or features on standard controls. Try to reuse HTML elements that do a subset of the functionality that the final control is intended to cover, and build from there. The following examples provide some general guidance on what to consider to ensure the additions are made accessible.

A Share Button

The example below shows a social media "share button" that has two functions: it shows how many people have already activated the button ("shared"), and allows users to press the button to activate the share function.

The custom button relies on CSS to style a regular <button> element, so that the basic functionality remains intact when it is rendered without CSS. For example, most screen readers will announce the button and its contents.

In addition, the action attribute of the <form> element references a server-side script that carries out the same functionality for cases when JavaScript is not supported.

Example:
Code snippet: HTML
<form action="path/to/submit">
  <button type="submit" id="share-btn" class="btn-primary">
    <span class="count">3</span>
    <span class="text">Shares</span>
  </button>
</form>
Code snippet: JavaScript
document.getElementById('share-btn').addEventListener('click', function(event){
  event.preventDefault();
  event.stopImmediatePropagation();

  var count = this.querySelector('.count');
  var text = this.querySelector('.text');

  count.textContent = parseInt(count.textContent) + 1;
  text.textContent = "Shared ✓";

  this.setAttribute("disabled", "true");
});

A Star Rating

A star rating usually consists of images of five stars that can be used to rate a particular item. A mouse user hovers over the stars and clicks one to select it. For example, if the user clicks on the third star from the left, the rating of the item is 3 of 5 stars.

To make this as accessible as possible, a form is used with its fields visually hidden. It contains 6 radio buttons, one for each star and another for 0 stars, which is checked by default. The labels for the radio buttons contain actual text (“1 Star”, “2 Stars”, …), and are also hidden visually. The form also contains a visually hidden submit button so that the form is not automatically submitted when keyboard users browse through the radio buttons.

The images of the stars are generated using SVG. The coloring animation of these stars is initiated through the CSS :focus and :hover pseudo classes, so that they can be activated using a mouse, keyboard, and other input methods. The :checked pseudo class and the general sibling selector ~ are used to indicate the selected, active, and inactive stars.

Example:
Code snippet: HTML
<form action="#" id="star_rating">
 <input value="0" id="star0" checked
  type="radio" name="rating" class="visuallyhidden">
  <label for="star0">
    <span class="visuallyhidden">0 Stars</span>
  </label>

  <input value="1" id="star1"
    type="radio" name="rating" class="visuallyhidden">
  <label for="star1">
    <span class="visuallyhidden">1 Star</span>
    <svg viewBox="0 0 512 512"><path d="M512 198.525l-176.89-25.704-79.11-160.291-79.108 160.291-176.892 25.704 128 124.769-30.216 176.176 158.216-83.179 158.216 83.179-30.217-176.176 128.001-124.769z"></path></svg>
  </label>

  <input value="2" id="star2"
    type="radio" name="rating" class="visuallyhidden">
  <label for="star2">
    <span class="visuallyhidden">2 Stars</span>
    <svg viewBox="0 0 512 512"></svg>
  </label>

  <input value="3" id="star3"
    type="radio" name="rating" class="visuallyhidden">
  <label for="star3">
    <span class="visuallyhidden">3 Stars</span>
    <svg viewBox="0 0 512 512"></svg>
  </label>

  <input value="4" id="star4"
    type="radio" name="rating" class="visuallyhidden">
  <label for="star4">
    <span class="visuallyhidden">4 Stars</span>
    <svg viewBox="0 0 512 512"></svg>
  </label>

  <input value="5" id="star5"
    type="radio" name="rating" class="visuallyhidden">
  <label for="star5">
    <span class="visuallyhidden">5 Stars</span>
    <svg viewBox="0 0 512 512"></svg>
  </label>

  <button type="submit" class="btn-small visuallyhidden focusable">Submit rating</button>

  <output></output>
</form>
Code snippet: CSS
#star_rating svg {
  width: 1em;
  height: 1em;
  fill: currentColor;
}
#star_rating label, #star_rating output {
  display:block;
  float:left;
  font-size: 2em;
  height: 1.2em;
  color: #036;
  cursor: pointer;
  border-bottom: 2px solid transparent;
}
#star_rating output {
  font-size: 1.5em;
  padding: 0 1em;
}
#star_rating input:checked ~ label {
  color: #999;
}
#star_rating input:checked + label {
  color: #036;
  border-bottom-color: #036;
}
#star_rating input:focus + label {
  border-bottom-style: dotted;
}
#star_rating:hover input + label {
  color: #036;
}
#star_rating input:hover ~ label,
#star_rating input:focus ~ label {
  color: #999;
}
#star_rating input:hover + label,
#star_rating input:focus + label {
  color: #036;
}
Code snippet: JavaScript
var radios = document.querySelectorAll('#star_rating input[type=radio]');
var btn = document.querySelector('#star_rating button');
var output = document.querySelector('#star_rating output');

var do_something = function(stars) {
  // An AJAX request could send the data to the server
  output.textContent = stars;
};

Array.prototype.forEach.call(radios, function(el, i){
  var label = el.nextSibling.nextSibling;
  label.addEventListener("click", function(event){
    do_something(label.querySelector('span').textContent);
  });
});

document.querySelector('#star_rating').addEventListener('submit', function(event){
  do_something(document.querySelector('#star_rating :checked ~ label span').textContent);
  event.preventDefault();
  event.stopImmediatePropagation();
});