alt-text

Try in Playground
jsx-a11y

oscar143

Best PracticeWarning

0

No tags

No CWE or CVE

jsx-a11y/alt-text

Enforce that all elements that require alt text have meaningful information. This is a critical component of accessibility for screen readers in order for them to understand the content. This rule checks for alternative text on: <img>, <area>, <input type="image">, and <object>.

Accessibility guidelines

  • WCAG 1.1.1

Resources

Ast Rule: html element


alt-text

How to write a rule
const TYPES = {
  string: getPropStringValue,
  sequence: getPropSequenceValue,
  array: getPropArrayValue,
  object: getPropObjectValue,
  object_element: getPropObjectValue,
  functiondefinition: () => {},
};

function getTag(node) {
  if (node && node.tag) {
    return node.tag.value;
  }
}

function getProp(attributes = [], prop = "") {
  if (!prop) return;

  return attributes.find((attribute) => {
    if (attribute && attribute.name && attribute.name.value) {
      return attribute.name.value === prop;
    }
  });
}

function getPropStringValue(value) {
  if (value.value === "true") {
    return true;
  }

  if (value.value === "false") {
    return false;
  }

  return value.value.replace(/^\"/g, "").replace(/\"$/g, "");
}

function getPropArrayValue(value) {
  const array = [];
  const arrayElements = value.elements;

  if (arrayElements.length) {
    for (const element of arrayElements) {
      array.push(TYPES[element.astType](element));
    }
  }

  return array;
}

function getPropObjectValue(value) {
  const object = {};
  const objectElements = value.elements;

  if (objectElements.length) {
    for (const element of objectElements) {
      object[element.name.value] = TYPES[element.value.astType](element.value);
    }
  }

  return object;
}

function getPropSequenceValue(value) {
  if (value.elements.length) {
    const element = value.elements[0];

    if (element) {
      return TYPES[element.astType](element);
    }
  }
}

function extractValue(attribute, extractor) {
  if (attribute) {
    // Null valued attributes imply truthiness.
    // For example: <div aria-hidden />
    if (attribute.value === null) return true;

    return extractor(attribute.value);
  }
}

function getValue(value) {
  return TYPES[value.astType](value);
}

function getPropValue(attribute) {
  return extractValue(attribute, getValue);
}

function debugObject(object) {
  for (const property in object) {
    console.log(`${property}: ${JSON.stringify(object[property])}`);
  }
}

function propHasValue(attribute) {
  const value = getPropValue(attribute);

  if (typeof value !== "boolean") {
    return !!value;
  }

  return false;
}

const presentationRoles = new Set(["presentation", "none"]);

function isPresentationRole(attributes) {
  const prop = getProp(attributes, "role");

  if (prop) {
    return presentationRoles.has(getPropValue(prop));
  }

  return false;
}

function imgRule(node) {
  const altProp = getProp(node.attributes, "alt");

  // Missing alt prop error.
  if (altProp === undefined) {
    if (isPresentationRole(node.attributes)) {
      const prop = getProp(node.attributes, "role");

      const error = buildError(
        node.start.line,
        node.start.col,
        node.end.line,
        node.end.col,
        `Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.`,
        "INFO",
        "BEST_PRACTICES"
      );

      const editUpdate = buildEditUpdate(
        prop.start.line,
        prop.start.col,
        prop.end.line,
        prop.end.col,
        `alt=""`
      );

      const fix = buildFix("replace the `role` attribute with `alt`", [
        editUpdate,
      ]);

      addError(error.addFix(fix));

      return;
    }

    // Check for `aria-label` to provide text alternative
    // Don't create an error if the attribute is used correctly. But if it
    // isn't, suggest that the developer use `alt` instead.
    const ariaLabelProp = getProp(node.attributes, "aria-label");

    if (ariaLabelProp !== undefined) {
      if (!getPropValue(ariaLabelProp)) {
        const error = buildError(
          node.start.line,
          node.start.col,
          node.end.line,
          node.end.col,
          "The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.",
          "INFO",
          "BEST_PRACTICES"
        );

        addError(error);

        return;
      }
    }

    // Check for `aria-labelledby` to provide text alternative
    // Don't create an error if the attribute is used correctly. But if it
    // isn't, suggest that the developer use `alt` instead.
    const ariaLabelledbyProp = getProp(node.attributes, "aria-labelledby");
    if (ariaLabelledbyProp !== undefined) {
      if (!getPropValue(ariaLabelledbyProp)) {
        context.report({
          node,
          message: "The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.",
        });
      }
      const error = buildError(
        node.start.line,
        node.start.col,
        node.end.line,
        node.end.col,
        "The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.",
        "INFO",
        "BEST_PRACTICES"
      );

      addError(error);

      return;
    }

    const error = buildError(
      node.start.line,
      node.start.col,
      node.end.line,
      node.end.col,
      `${node.tag.value} elements must have an alt prop, either with meaningful text, or an empty string for decorative images.`,
      "INFO",
      "BEST_PRACTICES"
    );

    const editAdd = buildEditAdd(
      node.tag.start.line,
      node.tag.start.col + node.tag.value.length,
      ` alt=""`
    );

    const fix = buildFix("add the `alt` attribute", [editAdd]);

    addError(error.addFix(fix));

    return;
  }

  // Check if alt prop value is undefined.
  if (propHasValue(altProp)) {
    return;
  }

  const error = buildError(
    node.start.line,
    node.start.col,
    node.end.line,
    node.end.col,
    `Invalid alt value for ${getTag(
      node
    )}. Use alt="" for presentational images.`,
    "INFO",
    "BEST_PRACTICES"
  );

  addError(error);
}

function objectRule(node) {
  const ariaLabelProp = getProp(node.attributes, "aria-label");
  const arialLabelledByProp = getProp(node.attributes, "aria-labelledby");
  const titleProp = getProp(node.attributes, "title");
  const hasLabel =
    propHasValue(ariaLabelProp) || propHasValue(arialLabelledByProp);
  const hasTitle = propHasValue(titleProp);

  // TODO: check for accessible child
  // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/rules/alt-text.js#L115
  if (hasLabel || hasTitle) {
    return;
  }

  const error = buildError(
    node.start.line,
    node.start.col,
    node.end.line,
    node.end.col,
    "Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.",
    "INFO",
    "BEST_PRACTICES"
  );

  addError(error);
}

function areaRule(node) {
  const ariaLabelProp = getProp(node.attributes, "aria-label");
  const arialLabelledByProp = getProp(node.attributes, "aria-labelledby");
  const hasLabel =
    propHasValue(ariaLabelProp) || propHasValue(arialLabelledByProp);

  if (hasLabel) {
    return;
  }

  const altProp = getProp(node.attributes, "alt");
  if (altProp === undefined) {
    const error = buildError(
      node.start.line,
      node.start.col,
      node.end.line,
      node.end.col,
      "Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.",
      "INFO",
      "BEST_PRACTICES"
    );

    addError(error);

    return;
  }

  // Check if alt prop value is undefined.
  if (propHasValue(altProp)) {
    return;
  }

  const error = buildError(
    node.start.line,
    node.start.col,
    node.end.line,
    node.end.col,
    "Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.",
    "INFO",
    "BEST_PRACTICES"
  );

  addError(error);
}

function inputImageRule(node) {
  // Only test input[type="image"]
  const typePropValue = getPropValue(getProp(node.attributes, "type"));
  if (typePropValue !== "image") {
    return;
  }

  const ariaLabelProp = getProp(node.attributes, "aria-label");
  const arialLabelledByProp = getProp(node.attributes, "aria-labelledby");
  const hasLabel =
    propHasValue(ariaLabelProp) || propHasValue(arialLabelledByProp);

  if (hasLabel) {
    return;
  }

  const altProp = getProp(node.attributes, "alt");
  if (altProp === undefined) {
    const error = buildError(
      node.start.line,
      node.start.col,
      node.end.line,
      node.end.col,
      '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
      "INFO",
      "BEST_PRACTICES"
    );

    addError(error);

    return;
  }

  // Check if alt prop value is undefined.
  if (propHasValue(altProp)) {
    return;
  }

  const error = buildError(
    node.start.line,
    node.start.col,
    node.end.line,
    node.end.col,
    '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
    "INFO",
    "BEST_PRACTICES"
  );

  addError(error);
}

function visit(node, filename, code) {
  switch (getTag(node)) {
    case "img":
      imgRule(node);
      break;
    case "object":
      objectRule(node);
      break;
    case "area":
      areaRule(node);
      break;
    case "input":
      inputImageRule(node);
      break;
    default:
      break;
  }
}

my-comp.jsx

Expected test result: no error

elements with atl prop

<img src="#" />
<img alt src="#" />
<img role="presentation" src="#" />
<img aria-label="" src="#" />
<object />
<area />
<input />
<input type="image" />
Add comment

Log in to add a comment


    Be the first one to leave a comment!

Codiga Logo
Codiga Hub
  • Rulesets
  • Playground
  • Snippets
  • Cookbooks
soc-2 icon

We are SOC-2 Compliance Certified

G2 high performer medal

Codiga – All rights reserved 2022.