ARIA directives

$aria adds accessibility-supporting defaults for the ARIA attributes that AngularTS manages automatically. It preserves authored ARIA, role, and tabindex attributes and should be treated as framework support for accessible applications, not as a complete WCAG conformance guarantee.

Use module.config({ $aria: ... }) when you need to adjust global ARIA support.

const app = angular.createModule('app', []);

app.config({
  $aria: {
    ariaCurrent: true,
    ariaCurrentToken: 'page',
    bindRoleForState: true,
    bindKeydown: true,
    diagnostics: false,
  },
});

ng.AriaConfig checks the standardized aria-current token used by router links. ARIA attributes authored in templates remain native HTML rather than framework-specific TypeScript contracts.

The public attribute-name surface follows the current WAI-ARIA states and properties and the developer-facing MDN ARIA attribute reference.

Use ng-aria-disable on an element when framework-authored ARIA behavior should not apply locally.

Set diagnostics: true in development or tests when you want $aria to warn about common authored mistakes on framework-managed interactive elements: positive tabindex, missing accessible names, missing ARIA reference targets, and interactive elements hidden with aria-hidden="true".

ng-state route links consume $aria defaults. Non-anchor and non-button route links receive role="link", tabindex="0", and Enter/Space keyboard activation when enabled. Links using data-state-active or data-state-exact receive framework-managed aria-current while current unless an authored aria-current value already exists.

Executable examples

<!doctype html>
<html>
  <head>
    <script type="module">
      import "../../utils/source-bootstrap.js";
      import "./ng-click-non-native.js";
    </script>
    <style>
      .interactive-card {
        border: 1px solid #cbd5e1;
        border-radius: 6px;
        padding: 12px;
        width: fit-content;
        cursor: pointer;
      }
    </style>
  </head>
  <body ng-app="ariaClickNonNativeDemo">
    <section ng-controller="AriaClickNonNativeCtrl as vm">
      <h2>Non-native ng-click accessibility</h2>
      <p>Count: {{ vm.count }}</p>
      <div class="interactive-card" ng-click="vm.count = vm.count + 1">
        Click or press Enter/Space
      </div>
      <p>
        AngularTS applies framework defaults (`role="button"` and
        `tabindex="0"`), so this non-button element is keyboard accessible without
        manual ARIA wiring.
      </p>
    </section>
  </body>
</html>

Non-native ng-click accessibility

Count: {{ vm.count }}

Click or press Enter/Space

AngularTS applies framework defaults (`role="button"` and `tabindex="0"`), so this non-button element is keyboard accessible without manual ARIA wiring.

<!doctype html>
<html>
  <head>
    <script type="module">
      import "../../utils/source-bootstrap.js";
      import "./form-validation.js";
    </script>
    <style>
      .field {
        margin-bottom: 12px;
      }
      .error {
        color: #b91c1c;
      }
      input.ng-invalid.ng-touched {
        border-color: #b91c1c;
      }
    </style>
  </head>
  <body ng-app="ariaFormValidationDemo">
    <section ng-controller="AriaFormValidationCtrl as vm">
      <form name="signupForm" novalidate>
        <div class="field">
          <label>Email</label>
          <input
            name="email"
            type="email"
            ng-model="vm.user.email"
            required
            aria-describedby="email-help"
          />
          <p id="email-help">Enter a valid email to continue.</p>
        </div>

        <div class="field">
          <label>Password</label>
          <input
            name="password"
            type="password"
            ng-model="vm.user.password"
            required
            minlength="8"
          />
        </div>

        <div class="field">
          <button type="button" ng-disabled="signupForm.invalid">Create account</button>
        </div>
      </form>

      <p class="error">
        Form invalid: {{ signupForm.invalid }}
        <br />
        Email invalid: {{ signupForm.email.invalid }}
        <br />
        Password invalid: {{ signupForm.password.invalid }}
      </p>
      <p>
        Notice how `aria-required` and `aria-invalid` are injected by `$aria` when
        form inputs are in their default states.
      </p>
    </section>
  </body>
</html>

Enter a valid email to continue.

Form invalid: {{ signupForm.invalid }}
Email invalid: {{ signupForm.email.invalid }}
Password invalid: {{ signupForm.password.invalid }}

Notice how `aria-required` and `aria-invalid` are injected by `$aria` when form inputs are in their default states.

<!doctype html>
<html>
  <head>
    <script type="module">
      import "../../utils/source-bootstrap.js";
      import "./ng-messages-live.js";
    </script>
    <style>
      .field {
        margin-bottom: 12px;
      }
      .error-box {
        margin-top: 8px;
        padding: 8px 10px;
        border: 1px solid #fecaca;
        background: #fff1f2;
        border-radius: 4px;
        max-width: 360px;
      }
    </style>
  </head>
  <body ng-app="ariaMessagesDemo">
    <section ng-controller="AriaMessagesCtrl as vm">
      <form name="contactForm" novalidate>
        <div class="field">
          <label>Email</label>
          <input
            name="email"
            type="email"
            ng-model="vm.user.email"
            required
            aria-describedby="email-hint"
          />
          <p id="email-hint">Use an address we can message.</p>
          <div
            class="error-box"
            ng-messages="contactForm.email.error"
            ng-show="contactForm.email.dirty || contactForm.submitted"
          >
            <p ng-message="required">Email is required.</p>
            <p ng-message="email">Please provide a valid email.</p>
          </div>
        </div>

        <button type="submit">Submit</button>
      </form>
    </section>
  </body>
</html>

Use an address we can message.

Email is required.

Please provide a valid email.