This episode covers accessibility and UX: ARIA attributes and keyboard navigation, semantic markup and accessible forms, responsive layout and mobile support, and internationalization and localization.

A good application is usable by everyone — including screen reader users, keyboard users, and devices with small screens. Accessibility isn't an optional feature; it's a quality standard.
Episode 17 covers ARIA attributes and keyboard navigation, semantic markup and accessible forms, responsive layout and mobile support, and internationalization and localization. Good accessibility almost always improves the UX for everyone.
ARIA (Accessible Rich Internet Applications) provides extra information to assistive technology. Angular Material and the CDK already use ARIA internally; custom components need to set it up themselves:
<button
(click)="bukaMenu()"
aria-haspopup="true"
[attr.aria-expanded]="menuTerbuka">
Menu
</button>aria-haspopup tells screen readers the button opens a menu, while [attr.aria-expanded] states the open-close status based on the menuTerbuka state. Angular binds ARIA attributes via [attr.*].
An application must be fully operable with the keyboard: Tab moves focus, Enter or Space activates, and Escape closes. The CDK a11y package provides FocusTrap, FocusMonitor, and LiveAnnouncer:
import { Component, inject } from '@angular/core';
import { LiveAnnouncer } from '@angular/cdk/a11y';
@Component({ selector: 'app-notif', standalone: true, template: `` })
export class NotifComponent {
private readonly announcer = inject(LiveAnnouncer);
kirimPesan(pesan: string): void {
this.announcer.announce(pesan, 'polite');
}
}LiveAnnouncer.announce reads a message aloud for screen reader users without disturbing the focus flow. This pattern is important when content changes dynamically — such as a "Data saved successfully" status.
Screen readers navigate based on structure. Use semantic elements: header, nav, main, h1 through h6, button, a, and label — not div with manual roles. Headings should form a logical hierarchy.
Every input must have an associated label. Without a label, screen reader users don't know what to fill in:
<div>
<label for="email">Alamat email</label>
<input id="email" type="email" [attr.aria-invalid]="email.invalid && email.touched" />
</div>label[for] and input[id] connect the two. aria-invalid tells assistive technology that the input has a problem. Avoid placeholders as the only label — placeholders disappear when typing and often have low contrast.
Connect error messages to inputs with aria-describedby:
<input id="email" aria-describedby="errEmail" />
<small id="errEmail" role="alert">Format email tidak valid</small>aria-describedby="errEmail" makes screen readers read that error message when the input gets focus. This makes validation not just visible, but also audible.
Start from small screen sizes, then enhance for larger screens. CSS grid and flexbox with relative units like rem and % let the layout follow the viewport size:
.kartu-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}repeat(auto-fit, minmax(240px, 1fr)) makes the grid automatically adjust the number of columns to the screen width — one column on a phone, several columns on desktop, without a media query for every breakpoint.
Aim for touch targets of at least 44 pixels, avoid hover as the only way to access a function, and test on real mobile browsers. Use the Angular CDK BreakpointObserver for layout logic that needs to know the screen size:
import { Component, inject, signal } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
@Component({ selector: 'app-layout', standalone: true, template: `` })
export class LayoutComponent {
private readonly observer = inject(BreakpointObserver);
readonly isMobile = signal(false);
constructor() {
this.observer.observe([Breakpoints.Handset]).subscribe((state) => {
this.isMobile.set(state.matches);
});
}
}Breakpoints.Handset is true when the viewport is a phone. The result can change menus, column counts, or panel layouts reactively.
Angular i18n uses the @angular/localize package. Add it to your project:
ng add @angular/localizeAfter that, mark text that needs translation with the i18n attribute:
<h1 i18n>Selamat datang di toko online</h1>
<p i18n>Belanja mudah dan aman</p>i18n marks text so it can be extracted into translation files. Run the extract command to produce the translation source file:
ng extract-i18n --output-path src/localeThis command produces messages.xlf, which you send to translators. Once translations are available, the application is built per language with --localize.
Build the application for several languages at once:
ng build --localizeThe result is a separate folder per language in dist. Angular also provides the DatePipe and CurrencyPipe, which adjust formatting to the active locale — so numbers and dates appear according to local conventions.
Key takeaways:
aria-describedby and role="alert" convey errors to screen readers.auto-fit grid and BreakpointObserver deliver responsive layouts.ng add @angular/localize enables i18n support.ng build --localize produces a separate build per language.In the next episode, episode 18, we'll cover architecture and patterns — feature modules and modular architecture, shared, core, and lazy modules, domain-driven design with a scalable folder structure, and clean architecture and separation of concerns.