## Introduction

While **Signal Forms** have been highly anticipated by many developers (myself included), the **Angular Aria** package is by far my favorite feature in **Angular 21**.

The most difficult part of starting with accessibility is learning how to implement it. Angular Aria is removing that entry barrier by providing amazing directives. That does not mean that you don't have to learn anything about **A11Y** now or in the future ;) 

## Cinema Application

As some of you may know, I consider myself a bookworm and comic addict, and because of that, I’ve always wanted to create a **Comic** **book** on my own, but was lacking drawing skills. With the evolution of **AI** - **Nano Banana**, I can finally do it! I’m also a big fan of learning by doing, and that is exactly what we will do today. We will create a simplified cinema application using every piece of the library. Now, let me introduce you to our Comic Book Heroes!

1.  PM - Elena  Fisher
    
2.  Software Engineer - Matteo Rossi
    
3.  UI/UX - Camille Dubois 
    

### Start with Accessibility, Stay away from Tech Debt

#### Comic Book #1 Transcript

1.  Elana - Let’s start with Accessibility.
    
2.  Camille \- Matteo, you can do that, right?
    
3.  Matteo \- Sure, we have Angular-Aria.
    

Starting with accessibility reduces **technical debt**, but its real value goes beyond that. An accessibility-first approach fundamentally changes **how you think** about the **user journey.** When accessibility is considered from the beginning, you’re forced to design for a **wider range** of users. This often leads to **clearer** information hierarchy, more intentional interaction patterns, and simpler flows overall. Instead of optimizing for a “default” user and adjusting later, accessibility pushes you to build **inclusive experiences by design.** There’s also a strong **practical** advantage. Applying accessibility after features are fully implemented is significantly harder. At that point, accessibility issues often expose deeper structural problems semantic markup, focus order, and component composition that require **large refactors**. Addressing these concerns early allows accessibility requirements to **shape the structure itself,** rather than fighting against it later. In that sense, accessibility-first isn’t just a best practice - it’s a way to build better foundations from the start, both for users and for the codebase.

#### Installation

1.  git clone [https://github.com/AdrianRomanski/angular-aria-cinema](https://github.com/AdrianRomanski/angular-aria-cinema)
    
2.  cd angular-aria-cinema
    
3.  Check out the starting point: `git checkout part-1-start`
    
4.  yarn install
    

### Day Selection

#### Comic Book #2 Transcript

1.  PM - We have a repertoire scheduled a week in advance.
    
2.  Camille \- Let’s limit the choice of days to this week.
    
3.  Matteo \- Select will be fine.
    

So the requirements are clear: users need to select a day from the current week . The obvious choice might seem to be a Select but before jumping to that conclusion, let’s take a closer look. Our first interaction with Angular Aria will actually be through a Combobox . Why introduce a Combobox when the end goal looks like a simple Select? Understanding this distinction early will help clarify accessibility patterns and component behavior as we move forward.

### Combobox Container

#### Code

“Note on color-coding.”

Before we start, I will introduce you to the color legend.

1.  Code related to the current example.
    
2.  Comment blocks used for grouping.
    

This approach will make the main parts stand out while still keeping the big picture clear.

Let’s start by adding our first import - Combobox from @angular/aria/combobox that will act as a wrapper for Select.

_**`libs/reservation/feature-movie-selection/src/lib/ui-day-selection.ts`**_

```

import { Combobox } from '@angular/aria/combobox';
```

First, we have to create a container that will help with **coordinating** input and popup, and **orchestrating** its behavior.

_**`libs/reservation/feature-movie-selection/src/lib/ui-day-selection.html`**_

```
<div class="day-selection">
    <h2 class="day-selection__title"> This Week </h2>
    <!--Combobox Container-->
    <div class="day-selection__select">&nbsp;&nbsp;&nbsp;&nbsp;
    </div>
    <!--End Of Combobox Container-->
</div>
```

Then apply the ngCombobox directive to the container and set readonly (we want to prevent users from typing).

_**`libs/reservation/feature-movie-selection/src/lib/ui-day-selection.html`**_

```
<div class="day-selection">
    <h2 class="day-selection__title"> This Week </h2>
    <!--Combobox Container-->
    <div class="day-selection__select" ngcombobox  readonly>
    </div>
    <!--End Of Combobox Container-->
</div>
```

#### Documentation

ngCombobox

*   Turns this container into a Combobox root, coordinating input with popup content.
    
    *   For more information, see [Combobox Role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/combobox_role).
        
*   Manages open/close state, keyboard interaction, and ARIA roles. 
    

readonly

*   Prevents free text input - selection must come from options.
    

#### **Visualization & A11Y Tree**

### ComboboxInput

With our container ready, let’s add an input block. 

#### Code 

Add another import from @angular/aria/combobox - ComboboxInput

```
import { 
&nbsp; Combobox, 
&nbsp; ComboboxInput
} from '@angular/aria/combobox';
```

We need to add more functionality:

1.  A display for the selected day or placeholder message using the **day()** method
    
2.  An input element for date selection 
    
3.  An arrow icon to indicate the open/closed state
    

```
<!--Combobox Container-->
<!--Code hidden for readability-->
    <!--ComboboxInput-->
    <div>
      <span> {{ day() }} </span>
      <input>
      <span> ▼ </span>
    </div>
    <!--End Of ComboboxInput-->
<!--End Of Combobox Container-->
```

Add the ngComboboxInput directive to the input, and it will register it as ComboboxInput.  

```
<!--Combobox Container-->
<!--Code hidden for readability-->
    <!--ComboboxInput-->
    <div>
      <span> {{ day() }} </span>
      <input ngcomboboxinput>
      <span> ▼ </span>
    </div>
    <!--End Of ComboboxInput-->
<!--End Of Combobox Container-->
```

Let’s check the accessibility tree.

We need to fix two problems: 

1.  combobox  is not labelled.
    
    1.  add id ‘week-label’  to the header.
        
    2.  add id ‘day-value’ to span with day() signal.
        
    3.  make an association with them by adding aria-labelledby=”week-label day-value” to the input.
        
2.  ▼ should not be visible, it's purely visual (open and close states are handled by aria-expanded).
    
    1.  hide it by adding aria-hidden=”true”
        

```
    <h2 id="week-label"> This Week    </h2>
    <!--Combobox Container-->
    <div  ngcombobox  readonly>
      <!--ComboboxInput-->
      <div>
        <span id="day-value">
          {{ day() }}
        </span>
        <input ngcomboboxinput aria-labelledby="week-label day-value">
        <span aria-hidden="true">
         ▼
        </span>
      </div>
      <!--End Of ComboboxInput-->
    
    <!--End of Combobox Container--></div >
```

Cool! The **icon** is no longer visible in the tree, and **combobox** has meaningful labels. 

#### Documentation

ngComboboxInput

*   Registers this input as the Combobox input.
    
*   Handles focus, keyboard navigation, and ARIA attributes.
    

id="week-label" id="day-value"

*   Identifiers for ARIA association with aria-labelledby.
    

aria-labelledby

*   Associates the input with human-readable text from the heading and selected value.
    
    *   For more information, see [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-labelledby).
        

aria-hidden

*   Hides the decorative icon (▼) from assistive technologies since it’s purely visual.
    
    *   For more information, see [aria-hidden](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden).
        

#### **Visualization & A11Y Tree**

_Closed State_

Tree : We got a proper label, it is focusable and expanded is set to false (closed)

_**Focused State**_

**Visual**: A proper focus indicator is visible **Tree**: **focused** is set to true, expanded is set to false (closed)

_**Open State**_

**Visual**: An arrow is **rotated**

**Tree**: **expanded** is set to true (open)

### Connecting Template With Trigger

After the user interacts with the input, we want to show the list of days. To achieve this, we'll connect a template that holds the list to a **trigger** using a combination of **Angular Aria** and **Angular CDK**.

#### Code

First, add another import from @angular/aria/combobox - ComboboxPopupContainer , then add a new library '@angular/cdk/overlay , and import CdkConnectedOverlay from it.

```
import { 
&nbsp; Combobox, 
&nbsp; ComboboxInput,
&nbsp; ComboboxPopupContainer
} from '@angular/aria/combobox';
import { CdkConnectedOverlay } from '@angular/cdk/overlay';
```

We need a new template with the ngComboboxPopupContainer directive.

```
<!--Combobox Container-->
<!--Code hidden for readability-->
  <!--ComboboxInput-->
  <!--Code hidden for readability-->
  <!--End Of ComboboxInput-->
  <!--Popup Container--> 
  <ng-template ngcomboboxpopupcontainer>
  </ng-template>
  <!--End of Popup Container-->
<!--End Of Combobox Container-->
```

Now we have to connect the trigger with an overlay. Let’s start by declaring a template reference variable to Input -  #trigger . We have to pass this variable to cdkConnectedOverlay . For that, we need to add another template inside ngComboboxPopupContainer . Two directives are required to make it work.

1.  \[cdkConnectedOverlayOpen\] \="true"
    
2.  \[cdkConnectedOverlay\] = "{origin: daySelection, usePopower: ‘inline’, matchWidth: true}"
    
    1.  origin \- Overlay is positioned relative to this element - daySelection
        
    2.  usePopower \- Overlay stays inside the current component DOM tree
        
    3.  matchWidth \- Overlay’s width matches the origin element’s width
        

```
<!--Combobox Container-->
<!--Code hidden for readability-->
       <div #trigger class="day-selection__select-trigger">
       <!-- Code hidden for readability -->
      </div>
      <!--ComboboxInput-->
      <!--End of Input-->
      <!--Popup Container--> 
        <ng-template ngcomboboxpopupcontainer>
          <ng-template  [cdkconnectedoverlay]="{
              origin:&nbsp; trigger, 
              usePopover: 'inline', 
              matchWidth: true
            }" [cdkconnectedoverlayopen]="true">
          </ng-template ></ng-template>
        
      <!--End of Popup Container-->
    
    <!--End of Combobox Wrapper-->
```

#### Documentation

ngComboboxPopupContainer

*   Marks where the combobox popup content is defined.
    
*   Ensures correct ARIA relationships between trigger and popup.
    

cdkConnectedOverlay

*   Positions the popup relative to the trigger element by passing dayOrigin .
    
    *   For more information, see [overlay](https://material.angular.dev/cdk/overlay/overview).
        

#### **Visualization & A11Y Tree**

Group has been added to the tree, it represents the overlay where the list will be displayed. It will be displayed after interaction with the element (clicking it or focusing it), as it’s lazy-loaded.

### Week Template

The last piece of this component is actually the template we want to show. 

It will hold a **List of day Options** for the current week.

#### Code

Add two imports: Listbox and Option from '@angular/aria/listbox' .

```
import { 
&nbsp; Combobox, 
&nbsp; ComboboxInput,
&nbsp; ComboboxPopupContainer
} from '@angular/aria/combobox';
import { Listbox, Option } from '@angular/aria/listbox';
import { CdkConnectedOverlay } from '@angular/cdk/overlay';
```

Add an ordered list with the ngListbox directive and aria-label=”Available days” .

Why an ordered list? Because the order of days matters, we don’t want to start a new week on Friday… or do we? 😄

```
<!--Combobox Container-->
<!--Code hidden for readability-->
    <!--ComboboxInput-->
    <!--Code hidden for readability-->
    <!--End of Input-->
    <!--Popup Container--> 
      <ng-template ngcomboboxpopupcontainer>
        <ng-template  <!--code hidden for readability-->
          <ol nglistbox aria-label="Avaible days" class="day-selection__select-popup">
          </ol>
         </ng-template ></ng-template>
       
     <!--End of Popup Container-->
<!--End Of Combobox Container--> 
    
```

If we get an unordered list, we need list items. Start by adding <li> with the ngOption directive. Loop through the days() signal that has an available days list. Now let’s add inputs:

1.  \[value\]=”day.value” \- value that will be emitted after selection
    
2.  \[label\]=”day.label” \- value that will be displayed as a label of the item
    

```
<!--Combobox Container-->
<!--Code hidden for readability-->
  <!--ComboboxInput-->
  <!--Code hidden for readability-->
  <!--End of Input-->
  <!--Popup Container-->
    <ol nglistbox aria-label="Avaible days" class="day-selection__select-popup">
    @for (day of days(); track day.value) {
      <li ngoption [value]="day.value" [label]="day.label" class="day-selection__select-option">
      </li>
     }
   </ol>
  <!--End Of Popup Container-->
<!--End Of Combobox Container--> 
```

There are 3 things we want to display: the selected state, the name of the day, and the date itself.

```
<!--Combobox Container-->
      <!--Code hidden for readability-->
      <!--Input-->
      <!--Code hidden for readability-->
      <!--End of Input-->
      <!--Popup Container-->
      <!--Code hidden for readability-->
      <ol nglistbox aria-label="Avaible days" class="day-selection__select-popup">
      @for (day of days(); track day.value) {
        <li ngoption [value]="day.value" [label]="day.label" class="day-selection__select-option">
            <span class="day-selection__select-option-check" aria-hidden="true">
              ✓
            </span>
            <span  class="day-selection__select-option-day">
              {{ day.dayName }}
            
            <span  class="day-selection__select-option-date">
              {{ day.label }}
           &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        </span ></span ></li>
      </ol>
      <!--End of Popup Container-->
<!--End of Combobox Container-->
```

#### Documentation

ngListbox

*   Defines an ARIA listbox.
    
    *   For more information, see [listbox role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/listbox_role).
        
*   Manages option selection, focus, and keyboard navigation.
    

\[(values)\]="selectedDay"

*   Enables two-way binding for the selected option.
    

ngOption

*   Registers this element as a selectable option inside the listbox and applies ARIA role="option".
    
    *   For more information, see [option role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/option_role).
        

\[value\]="day.value"

*   The value emitted when this option is selected.
    

\[label\]="day.label"

*   An accessible label used by screen readers
    

#### Visualization & A11Y Tree

_**Open List of Days**_

After opening the listbox, focus stays on it, and expanded is set to true.

_**Select a Day**_

After selecting the day, selected is set to true.

### Feel the User Journey

#### Code

```
<div class="day-selection">
  <h2 id="week-label" class="day-selection__title">This Week</h2>
  <!--Combobox Container-->
  <div ngcombobox readonly>
    <!--ComboBox Input-->
    <div #trigger class="day-selection__select-trigger">
      <span id="day-value" class="day-selection__select-value">
        {{ day() }}
      </span>
      <input aria-labelledby="week-label day-value" ngcomboboxinput class="day-selection__select-input">
      <span class="day-selection__select-arrow" aria-hidden="true">
        ▼
      </span>
    </div>
    <!--End Of ComboBox Input-->

    <!--Popup Container-->
    <ng-template ngcomboboxpopupcontainer>
      <ng-template [cdkconnectedoverlay]="{origin: trigger, usePopover: 'inline', matchWidth: true}" [cdkconnectedoverlayopen]="true">
                  <ol nglistbox [(values)]="selectedDay" aria-label="Available days" class="day-selection__select-popup">
                    @for (day of days(); track day.value) {
                      <li ngoption [value]="day.value" [label]="day.label" class="day-selection__select-option">
                        <span class="day-selection__select-option-check" aria-hidden="true">
                          ✓
                        </span>
                        <span class="day-selection__select-option-day">
                          {{ day.dayName }}
                        </span>
                        <span class="day-selection__select-option-date">
                          {{ day.label }}
                        </span>
                      </li>
                    }
                  </ol>
      </ng-template>
    </ng-template>
    <!--End Of Popup Container-->
  </div>
  <!--End Of Combobox Container-->
</div>
```

#### Comic Book #3 Transcript

1.  PM - Good, so we can see this is the current week.
    
2.  Camille \- Does it show the number of days?
    
3.  Matteo \- It does, and navigation is also working. Same with selecting items.
    

If we really want to be sure that our solution is working, we need to get our hands dirty 😀 Screen Reader testing is not that hard if you do it step by step. First, we need to turn on the software. But before we jump in, here’s another color legend 😂

*   Pressed Key
    
*   Assistive Technology
    
*   Code From Example
    

#### Voiceover MacBook

1.  Turn on the Voiceover
    
2.  TAB
    
3.  “Current Week Select a day, clickable list box popup collapsed combobox”
    
    1.  “Current Week Select a day” \- these values come from aria-labelledby="week-label day-value"
        
    2.  “clickable” indicates that the element is interactive and can be activated
        
    3.  “listbox” - comes from role="listbox" that was applied by ngListbox
        
    4.  “popup” - comes from aria-haspopup="listbox" that was applied by ngComboboxPopupContainer
        
    5.  “collapsed” - comes from aria-expanded="false" and means that the listbox is currently closed
        
    6.  “combobox” - comes from role="combobox" that was applied by ngCombobox
        

It does not look readable at first sight, but after some practice it becomes more logical. We should interpret it this way: “This is an interactive control. It’s a combobox that opens a listbox popup, and that popup is currently closed.” Let’s activate it 🙂

1.  Enter
    
2.  “Expanded, list box 7 items”
    
    1.  expanded - comes from aria-expanded="true", which means that the listbox is currently open
        
    2.  7 items \- the number of elements with role=”option” inside the element with role=”listbox”. The option role was applied by ngOption
        
3.  Escape
    
4.  “Collapsed”
    
5.  Space
    
6.  ArrowDown
    
7.  “Monday January 12 not selected, menu item, (1 of 7)”
    
8.  ArrowDown
    
9.  “Tuesday January 13 not selected, menu item, (2 of 7)”
    
10.  Enter
     
11.  “collapsed,  13 January” means the listbox is closed and the selected item is 13 January
     

### Homework

#### Comic Book #4 Transcript

1.  Matteo \- I have a feeling we might be missing something.
    
2.  Camille \- Good point! What about holidays?
    
3.  PM - How about we delegate this to our trainee?
    

Our trainee is joining the room:

1.  Software Engineer – Lucas Meyer
    
2.  PM - Elena  Fisher
    
3.  **Software Engineer** - Matteo Rossi
    
4.  UI/UX - Camille Dubois 
    

#### Comic Book #5 Transcript

1.  PM - Lucas did an amazing job at the tech interview
    
2.  Matteo \- Perfect, we are doing Day Selector, but the disabled state is missing.
    
3.  Camille \- We need it for holidays in case of emergency
    
4.  Lucas Meyer \- Sure! Will do that.
    

#### Tasks

1.  Add a disabled state using the [option-directive](https://angular.dev/guide/aria/listbox#option-directive)
    
2.  Add a story that features one or more holidays
    

#### Comic Book #6 Transcript

1.  Lucas Meyer \- It’s working!
    
2.  PM - So, did we just finish the sprint?
    
3.  Matteo \- Let’s hand it off to QA and catch up next Monday.
    
4.  Camille \- We can select a day. What’s next?
    

#### Solution

`git checkout part-1-finish`

If you enjoyed this article, I’m happy to share that we will be creating a new component every second Monday. If you’re reading this in the future, just jump into the next lesson! 😀

[https://github.com/AdrianRomanski/angular-aria-cinema](https://github.com/AdrianRomanski/angular-aria-cinema)
