Linting Custom Components

Link to Linting Custom Components copied to clipboard

A guide to using the axe DevTools Linter for linting custom components

Free Trial

Introduction

This article shows how to use axe DevTools Linter to find accessibility errors in custom components.

You can provide a mapping between your custom components and the HTML elements that are generated by those components so axe DevTools Linter can determine whether they violate accessibility rules. For instance, if you create a custom-image component that emits a standard img element, axe DevTools Linter can use this information (which you provide in the global-components configuration option) to check whether the use of your custom-image components have the required attributes or text content for it to be accessible.

By specifying other configurations you can:

  • Map your custom component's attributes to a standard element's attributes.
  • Map an attribute in your custom component to a standard HTML element's text content (which is the text node contained within the element such as the text inside button tags: <button> text content </button>).
  • Map WAI-ARIA attributes from your custom component to standard HTML elements using a wildcard attribute (aria-*) rather than specifying each individually.

A Custom Component Linting Walkthrough

When you use axe DevTools Linter to lint source code, you provide a JSON body containing source and configuration in your HTTP request. For example, the following HTML shows the use of the img element (this is a greatly oversimplified example only to demonstrate linting rather than an actual real-world example):

<img src="path/to/image.jpg"/>

The JSON body of the request to be sent to axe DevTools Linter would look like this:

{ 
  "source": "<img src=\"path/to/image.jpg\"/>",
  "filename": "image-demo.html"
}
note

You send this JSON to axe DevTools Linter as a REST POST request to the /linter-source endpoint. For more information, see The Lint Endpoint in the reference documentation.

To follow along with this walkthrough, you can use any REST tool that can send POST requests with JSON request bodies. The examples that follow all show the request body that was sent to axe DevTools Linter and the JSON response body which shows the accessibility errors found by axe DevTools Linter.

Because this img element doesn't have an alt attribute, you'll get an accessibility error from axe DevTools Linter:

{
  "report": {
    "errors": [
      {
        "column": 1,
        "description": "Ensures <img> elements have alternate text or a role of none or presentation",
        "endColumn": 31,
        "helpURL": "https://dequeuniversity.com/rules/axe/4.4/image-alt?application=axe-linter",
        "lineContent": "<img src=\"path/to/image.jpg\"/>",
        "lineNumber": 1,
        "linterType": "html",
        "ruleId": "image-alt"
      }
    ]
  }
}

A Custom Image Component

The following sample shows example usage of a custom-image custom component:

<custom-image src="path/to/image.jpg"></custom-image>

To send the HTML to axe DevTools Linter using a POST request, use the following as the JSON body:

{
  "source": "<custom-image src=\"path/to/image.jpg\"></custom-image>",
  "filename": "custom-image.html"
}

The server responds with no accessibility errors because there is no mapping between custom-image and img so axe DevTools Linter cannot flag a missing alt attribute:

{
  "report": {
    "errors": []
  }
}

Mapping custom-image to img

If you provide a mapping between custom-image to img, axe DevTools Linter can map your custom component as a standard HTML element and locate accessibility errors. You can specify the mapping by using the global-components configuration option (part of the config object):

{
  "config": {
    "global-components": {
      "custom-image": "img"
    }
  },
  "filename": "c-image.html",
  "source": "<custom-image src=\"path/to/image.jpg\"></custom-image>\n\n"
}

axe DevTools Linter now responds with the following:

{
  "report": {
    "errors": [
      {
        "column": 1,
        "description": "Ensures <img> elements have alternate text or a role of none or presentation",
        "endColumn": 54,
        "helpURL": "https://dequeuniversity.com/rules/axe/4.4/image-alt?application=axe-linter",
        "lineContent": "<custom-image src=\"path/to/image.jpg\"></custom-image>",
        "lineNumber": 1,
        "linterType": "html",
        "ruleId": "image-alt"
      }
    ]
  }
}

You can also indicate the same mapping as above with either of these syntaxes:

{
  "config": {
    "global-components": {
      "custom-image": {
        "element": "img"
      }
    }
  }
}

Or, alternatively, by abbreviating element as el:

{
  "config": {
    "global-components": {
      "custom-image": {
        "el": "img"
      }
    }
  }
}
important

When you use an element mapping as shown above, all the attributes from the custom component are copied to the emitted element, and that emitted element is linted.

Fixing the Accessibility Problem

You can add an alt attribute to your custom-image to fix the accessibility problem (as shown below with the JSON body of the request):

{
  "config": {
    "global-components": {
      "custom-image": "img"
    }
  },
  "filename": "c-image.html",
  "source": "<custom-image src=\"path/to/image.jpg\" alt=\"alt text\"></custom-image>\n\n"
}

The server responds with the following empty errors array because your custom component has the required alt attribute (which was copied—with all other attributes on the custom-image component—to the emitted img element):

{
  "report": {
    "errors": []
  }
}

Mapping the alternative-text Attribute

If your custom image component instead uses a different attribute to indicate alternative text, you can specify that attribute in the configuration. For instance, suppose your custom-image component uses an alternative-text attribute instead of alt, as shown below:

<custom-image src="path/to/image.jpg" alternative-text="alt text"></custom-image>

In this case, you could specify a mapping between the alternative-text attribute and the alt attribute as shown with the attributes array in the JSON request body shown below:

{
  "config": {
    "global-components": {
      "custom-image": {
        "element": "img",
        "attributes": [
          {
            "alternative-text": "alt"
          }
        ]
      }
    }
  },
  "filename": "c-image.html",
  "source": "<custom-image src=\"path/to/image.jpg\" alternative-text=\"alt text\"></custom-image>\n\n"
}
note

Note that the global-components configuration is slightly different from the earlier mapping of one custom component to one HTML element. With only elements, you use a mapping from a string ("custom-image") to another string ("img"). With the inclusion of the attributes array, you are now required to use the element (or el) property to specify the emitted HTML element.

axe DevTools Linter responds with the following because the alt-text rule has been satisfied by your alternative-text attribute:

{
  "report": {
    "errors": []
  }
}
important

Because you specified the attributes array in the configuration, when the server maps from custom-image to img, only the attributes specified in the attributes array are copied to the emitted HTML element.

You can also abbreviate attributes as attrs:

{
  "config": {
    "global-components": {
      "custom-image": {
        "attrs": [
          {
            "alternative-text": "alt"
          }
        ],
        "element": "img"
      }
    }
  }
}

Special Attribute Values: <text> and aria-*

Suppose you use a custom-button component as follows:

<custom-button aria-controls="expand-region" aria-expanded="false" aria-colindex="1" message="Show Region"></custom-button>

(The custom button will, using JavaScript and CSS that isn't included here, hide and show a div.)

There are two problems with this usage:

  1. If you map this custom-button component directly to a button element, there will be no text content to be displayed on the button. The component author's intent, however, is that the message attribute should be used as text content: <button> value of the message attribute </button>
  2. The emitted button element has an implicit role of button so the aria-colindex attribute is incorrect and should be removed.

If you send the above code to axe DevTools Linter (with no global-components mapping), you receive this response:

{
  "report": {
    "errors": []
  }
}

The Special <text> Value

To address the first problem (text content for the button element coming from a message attribute), you can use the special <text> value which maps an attribute to the emitted element's text content. In this case the message attribute's text should be copied to the emitted button element's text content.

To configure the request to alert axe DevTools Linter that the message attribute should be considered as text content for the HTML button element, you can use the special <text> value and send the following request:

{
  "config": {
    "global-components": {
      "custom-button": {
        "attributes": [
          {
            "message": "<text>"
          }
        ],
        "element": "button"
      }
    }
  },
  "filename": "aria-button.html",
  "source": "<custom-button aria-controls=\"expand-region\" aria-expanded=\"false\" aria-colindex=\"1\" message=\"Show Region\"></custom-button>\n"
}

Because you defined the message attribute as <text>, you told axe DevTools Linter to consider that attribute as replacing textual content of the HTML button element with the value of the message attribute.

Unfortunately, by using the attributes array, the only attribute that was passed through to the emitted button element was only the message attribute; any attributes not in the attributes array are not passed through. This means that the incorrect aria-colindex was not caught by the server.

Using aria-*

You can use the special aria-* value to pass all the ARIA attributes, as shown below:

{
  "config": {
    "global-components": {
      "custom-button": {
        "attributes": [
          {
            "message": "<text>"
          },
          "aria-*"
        ],
        "element": "button"
      }
    }
  },
  "filename": "aria-button.html",
  "source": "<custom-button aria-controls=\"expand-region\" aria-expanded=\"false\" aria-colindex=\"1\" message=\"Show Region\"></custom-button>\n"
}

The server responds with:

{
  "report": {
    "errors": [
      {
        "column": 1,
        "description": "Ensures ARIA attributes are allowed for an element's role",
        "endColumn": 124,
        "helpURL": "https://dequeuniversity.com/rules/axe/4.4/aria-allowed-attr?application=axe-linter",
        "lineContent": "<custom-button aria-controls=\"expand-region\" aria-expanded=\"false\" aria-colindex=\"1\" message=\"Show Region\"></custom-button>",
        "lineNumber": 1,
        "linterType": "html",
        "ruleId": "aria-allowed-attr"
      }
    ]
  }
}

This error occurs because the button element has an implicit role="button" and using aria-colindex is invalid with buttons, and all ARIA attributes are copied to the emitted element due to the aria-* attribute value.

Passing All Attributes Implicitly

Note that if you'd used only element mapping (where the mapping does not use the attributes array), all attributes would be, by default, copied to the button element as shown below:

{
  "config": {
    "global-components": {
      "custom-button": "button"
    }
  },
  "filename": "aria-button.html",
  "source": "<custom-button aria-controls=\"expand-region\" aria-expanded=\"false\" aria-colindex=\"1\" message=\"Show Region\"></custom-button>\n"
}

With this server response, you can see that all of the attributes from custom-button are checked for accessibility problems, and two errors are found:

{
  "report": {
    "errors": [
      {
        "column": 1,
        "description": "Ensures ARIA attributes are allowed for an element's role",
        "endColumn": 124,
        "helpURL": "https://dequeuniversity.com/rules/axe/4.4/aria-allowed-attr?application=axe-linter",
        "lineContent": "<custom-button aria-controls=\"expand-region\" aria-expanded=\"false\" aria-colindex=\"1\" message=\"Show Region\"></custom-button>",
        "lineNumber": 1,
        "linterType": "html",
        "ruleId": "aria-allowed-attr"
      },
      {
        "column": 1,
        "description": "Ensures buttons have discernible text",
        "endColumn": 124,
        "helpURL": "https://dequeuniversity.com/rules/axe/4.4/button-name?application=axe-linter",
        "lineContent": "<custom-button aria-controls=\"expand-region\" aria-expanded=\"false\" aria-colindex=\"1\" message=\"Show Region\"></custom-button>",
        "lineNumber": 1,
        "linterType": "html",
        "ruleId": "button-name"
      }
    ]
  }
}

The above example shows that a practical first step when beginning to lint custom components would be to start with an element mapping (thereby copying all attributes to the emitted, standard HTML element) and then seeing what attributes need to be added to the configuration (whether the custom component's attributes should be mapped to different attributes or whether you need to use <text> or aria-*).

See Also