- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Making an Angular App Work for Everyone
TaskFlow is a small Angular task-management dashboard with three routed views: a task list, a task-detail panel, and an 'Add Task' form. The starter code renders correctly in a browser but fails every accessibility audit: buttons are plain divs, form errors are invisible to assistive technology, modals trap neither focus nor keyboard, route changes are silent, and no automated checks exist. Learners repair each layer of the application in turn, treating the real user — including keyboard-only and screen-reader users — as the acceptance criterion.
Lab Info
Table of Contents
-
Challenge
Step 1: Explore TaskFlow and Understand Its Accessibility Gaps
TaskFlow is a small Angular task-management dashboard with three routed views: a task list, a task-detail panel, and an Add Task form. Every feature works when you click through it with a mouse — tasks load, the detail panel slides in, the form submits. But TaskFlow was built without ever considering a keyboard-only user or a screen-reader user, and this lab spends the next four steps fixing that, one layer at a time.
Before changing any code, run the existing test suite and open the app in a browser. Notice what a sighted mouse user never has to think about: which element currently has focus, what happens when you press Tab repeatedly, and whether pressing Escape or Enter does what you'd expect. ## Navigate with only the keyboard
Unplug your mouse, mentally if not literally, and tab through each of the three views. In the task list you'll find delete controls that never receive focus, because they're
<div>elements with a click handler rather than<button>elements. Click one with the mouse and a confirmation dialog opens, but Tab walks focus straight out of it onto the page behind, and Cancel never closes it. In the task-detail panel, tabbing into the trigger and pressing it gives you no indication — visually or programmatically — of whether the panel opened. In the Add Task form, submitting invalid input shows a red message that a sighted user notices immediately and a screen-reader user never hears, because nothing tells assistive technology that new, important text just appeared.This is the core distinction the rest of the lab works from: semantic HTML gives elements built-in behavior and meaning for free (a
<button>is focusable and clickable with Enter or Space without any extra code), while ARIA attributes let you describe relationships and states that HTML alone can't express — but only when you keep them synchronized with real component state. ## How a screen reader experiences the pageA screen reader doesn't read pixels; it reads the accessibility tree the browser builds from the DOM. Every element gets a computed role, an accessible name, and a set of states. A
<div>with a click handler has rolegenericand no accessible name unless you add one — a<button>gets rolebuttonfor free. An element witharia-expanded="false"announces as collapsed; if that attribute is hard-coded and never changes, the announcement is simply wrong, which is worse than saying nothing.Keep this tree in mind as your acceptance criterion for the rest of the lab: not "does it look right" but "what would a screen reader say, and can a keyboard user reach and operate it?" The tests in each following step check exactly that — rendered roles, names, states, and relationships — rather than checking visual appearance. ## What's ahead
Step 2 repairs the task list's foundational markup. Step 3 binds live ARIA state to the task-detail panel. Step 4 extracts a reusable focus-trap directive and an accessible confirm dialog using Angular CDK. Step 5 makes route changes perceivable with page titles and focus management, and closes the loop with an automated Axe-core check. Each step builds directly on the files the previous step touched, so work through them in order.
info> If you get stuck, you can refer to the provided solution code for each task, available in the
solutionfolder. -
Challenge
Step 2: Replace Non-Semantic Elements and Add Meaningful Screen-Reader Feedback
The task list is where most users spend their time in TaskFlow, and it's also where the accessibility gaps are most obvious: interactive elements built from
<div>s, list items with no list semantics, and error text that appears silently. You'll repair all three insrc/features/task-list/TaskList.ts. ## Semantic elements come with behavior built inA native
<button>is focusable by default, activates on both Enter and Space, and is announced with rolebuttonand whatever text oraria-labelit carries — none of which you have to implement yourself. A<div>with a(click)handler has none of that; you'd have to addtabindex, akeydownhandler for Enter and Space, and aroleattribute by hand, and it's easy to miss one. The fix is almost always to reach for the right element first, and only add ARIA when no native element fits. ## Lists need list semantics, and loops need stable identityA screen reader announces
<ul>/<li>structure with position and count ("item 3 of 12"), information a stack of<div>s never provides. Separately, Angular's@forcontrol-flow block requires atrackexpression so it knows which DOM node corresponds to which data item across re-renders — get this wrong with an index-based track and reordering a list will move focus to the wrong row instead of following the task the user was working with. ## Announcing changes without moving focusSome updates matter but don't warrant interrupting where the user is working — a validation error appearing while they're still typing in a field is exactly this case.
aria-live="polite"(paired withrole="status") tells assistive technology to announce new content in that region whenever it changes, without touching keyboard focus. It only works, though, if the element already exists in the DOM before the update — swapping in a brand-new element with the message already inside it won't trigger the announcement. -
Challenge
Step 3: Bind Dynamic ARIA States to the Task-Detail Panel
The task-detail panel is a disclosure pattern: a trigger button that reveals a panel. HTML has no native element for this, so it depends entirely on ARIA attributes staying in sync with real component state. You'll work in
src/features/task-detail/TaskDetailPanel.ts. ##aria-expandedmust track real statearia-expanded="true"or"false"on a trigger tells assistive technology whether the thing it controls is currently visible. If you bind it to a signal that actually flips, the announcement stays accurate through every interaction; if you set it once and forget it, users will be told the panel is closed while it's sitting open in front of them — an actively misleading announcement is worse than no announcement at all. ## Connecting a trigger to the element it revealsaria-controlson the trigger, pointing at theidof the panel it reveals, tells a screen reader exactly which element will appear or disappear. The two ids only work as a pair — a staticidon the panel and a dynamicaria-controlson the button (or the reverse) will silently point at nothing once tasks change, so both need to be derived from the same source. ## Describing an action by what it acts onWhen a page has many repeated action buttons — one "complete" or "archive" button per row — visual proximity is how a sighted user tells them apart, but a screen reader user navigating by button name alone gets no such cue unless the button is described by its row.
aria-describedbylets you point a control at the id of another element on the page, so its accessible description is built from content you already have rather than duplicated text. -
Challenge
Step 4: Build a Reusable Focus-Trap Directive and an Accessible Modal Component
TaskFlow's confirm dialog currently lets keyboard focus escape behind it while it's open — pressing Tab a few times lands you back on the page underneath, which a sighted mouse user never notices but a keyboard user finds disorienting or trapped in the opposite sense. Angular CDK ships a
ConfigurableFocusTrapfor exactly this problem; you'll wrap it in your own reusable directive. ## Trapping focus inside an open dialogA focus trap constrains Tab and Shift+Tab so they cycle only through elements inside the trapped region, and returns focus to the first focusable element when the trap activates. CDK's
FocusTrapFactorybuilds one for a given host element; the trap itself needs to be created when the dialog opens and destroyed when it closes, or it keeps intercepting focus on a page that has moved on. You'll drive that lifecycle from a signal input so any component can activate the trap just by setting a boolean. ## Announcing state changes that have no visual equivalentA sighted user sees the dialog appear; a screen-reader user needs to be told. CDK's
LiveAnnouncerservice pushes a message into a live region for you, so you don't have to manage one by hand the way you did for the error banner in Step 2. The trick is firing it exactly once, at the moment the trap activates — not on every change-detection cycle while the dialog stays open. ## Composing the directive into a componentA directive that manages focus is only half the feature. The
ConfirmDialogcomponent insrc/shared/dialog/ConfirmDialog.tsdoesn't use yourFocusTrapdirective yet, so you'll import it, applyfocusTrapto the dialog element with[trapActive]bound toisOpen(), and make the dialog close and hand focus back to whatever triggered it in the first place. Returning focus to the trigger on close is what makes the whole interaction feel continuous instead of dropping the keyboard user back at the top of the page. The task list's delete buttons already open this dialog, so withnpm run devrunning you can watch the difference in the browser before and after the task. -
Challenge
Step 5: Identify Routed Views and Enforce Checks with Axe-core
A single-page app can navigate to an entirely new view without a full page load, which means none of the signals a browser normally gives a screen-reader user — a new document title, a fresh top of page — happen automatically. You'll make TaskFlow's route changes perceivable, then close the loop with an automated check that verifies the fixes actually hold. ## Titles are a route change's headline
Angular's
TitleStrategyis the hook the router calls on every navigation to decide the document title; overriding it in one place means every route gets a correct, distinct title without repeating logic in each component. A screen reader announces the new document title on navigation in most browsers, which makes it one of the cheapest, highest-value fixes available for a routed app. ## Focus needs somewhere to landA descriptive title tells a screen-reader user what changed; moving focus tells them where to start reading. Sending focus to a landmark element (a
<main>withtabindex="-1"so it's programmatically focusable without being reachable by Tab) after everyNavigationEndevent gives keyboard and screen-reader users a consistent, predictable place to resume after every route change, matching the behavior a full page load would have given them for free. ## Verifying it holds with an automated auditManual keyboard and screen-reader testing catches issues automated tools miss, but Axe-core catches the mechanical ones — missing labels, invalid ARIA, insufficient contrast — quickly and repeatably inside a test run. Running it against the Add Task form surfaces the one violation left in this lab: labels and inputs that share no
id/forrelationship, so the audit can't determine each field's accessible name even though the label text is right there on the page. -
Challenge
Step 6: Run the application
Every task is done, and each one was graded the moment you completed it. What you have not done yet is watch the whole application run at once — which is the only place the pieces you wrote separately become a single working program.
Start the application
Open the Terminal tab and start the dev server:
npm run devIt serves the application on port 4200. Switch to the Web Browser tab to use it: the page is running the functions you implemented in the previous steps, so what you see is your own code rather than a demonstration of it.
Leave the server running while you look around. It reloads on save, so you can change a file and see the result without restarting anything. Try deleting a task: the confirm dialog keeps Tab inside it, Cancel returns focus to the delete button, and Delete removes the task. ### Run the checks yourself
The lab ran each task's test for you as you went, but they are ordinary tests and you can run them whenever you like. The whole suite:
npx --no-install ng test --no-watch --reporters=dotOr one task at a time:
bash runTest.sh "task 2.1"Each failure names the task it belongs to, so the output tells you which step to go back to rather than which line to stare at. Two things worth trying before you finish: break something on purpose and watch the matching check fail, then put it back. Knowing what a failure looks like is worth as much as knowing what a pass looks like, and it costs you nothing here.
When you are done, everything you wrote is still in the editor tabs — the lab is yours to keep reading.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.