Most Power Apps developers treat accessibility as a final checkbox — that approach produces apps that fail real users. This expert-level lesson covers everything from Canvas App rendering internals to WCAG 2.1 AA implementation, screen reader announcement patterns, focus management for modals, and accessible data tables, so your enterprise apps genuinely work for every user.

Picture this: your organization has just deployed a beautifully crafted Canvas App for HR onboarding — clean UI, snappy data connections, polished color scheme. Six weeks later, legal sends a note. An employee using a screen reader filed a complaint because they can't complete the onboarding workflow without sighted assistance. Your app works perfectly for 95% of users and is completely unusable for the other 5%. In many jurisdictions, particularly for enterprise software deployed under government contracts or in regulated industries, that's not a UX problem — it's a compliance failure with real legal consequences.
Accessibility in Canvas Apps is one of the most underestimated engineering challenges in the Power Platform ecosystem. Most developers treat it as an afterthought, sprinkling in some AccessibleLabel properties at the end and calling it done. That approach produces apps that technically check some boxes but fail completely when a real person with a disability tries to use them. Genuine accessibility compliance requires rethinking your app architecture from the ground up — how you structure controls, manage focus, write formulas, and design visual hierarchy.
By the end of this lesson, you will have the knowledge and patterns to build Canvas Apps that genuinely meet WCAG 2.1 AA standards — the threshold required by most enterprise and government accessibility policies. You'll understand not just which properties to set, but why the browser-based rendering model works the way it does, where Power Apps' own accessibility support breaks down, and how to work around its limitations with advanced techniques.
What you'll learn:
Before you can make a Canvas App accessible, you need to understand what you're actually building. Canvas Apps don't render as semantic HTML in the traditional sense. When Power Apps runs in a browser, it renders your app inside a single <div> container using an absolute positioning model. Every control is placed using pixel coordinates derived from your design canvas. The browser doesn't naturally understand the relationships between your controls — Power Apps constructs an accessibility tree on top of this rendering model using ARIA attributes.
This is fundamentally different from building a webpage where a <nav> element automatically signals navigation to a screen reader, or a <button> element inherently receives focus and keyboard events. In Canvas Apps, when you drop a Button control on the canvas, Power Apps generates something like:
<div
role="button"
aria-label="Submit Request"
tabindex="0"
aria-disabled="false">
Submit Request
</div>
The role="button" is doing all the semantic work. If you haven't set an AccessibleLabel, the aria-label might be empty or default to the control name (Button1). If you haven't configured TabIndex, the tab order might be determined by the order you added controls to the screen, not their visual position.
Understanding this rendering model has three immediate implications for your work:
First, the visual layout and the accessibility tree can diverge. You could have a form where the visible reading order goes Label → Input → Label → Input, but the tab order goes Input → Input → Label → Label because of the order controls were added. Sighted users navigate by vision and never notice. Screen reader users navigate by tab order and get a completely disorienting experience.
Second, dynamic content is genuinely hard. When your app updates a Gallery based on a filter, or shows a validation error, the DOM changes — but screen readers don't automatically announce those changes unless you explicitly configure live regions. Power Apps has limited native support for this, so you'll need workarounds.
Third, some controls are accessibility black holes. The HTML Text control, Camera control, and certain custom components render in ways that break assistive technology support entirely. You need to know which controls to avoid and what to use instead.
Architecture tip: Enable the "Improved accessibility" setting in your app's Settings > Upcoming Features before you start building. This activates updated accessibility behaviors and is likely to become the default. Building without it and retrofitting later is painful.
WCAG 2.1 AA is organized around four principles — Perceivable, Operable, Understandable, and Robust — each containing specific success criteria. Let's map the most critical criteria to concrete Canvas App implementation decisions.
1.1.1 — Non-text Content (Level A): Every non-text element must have a text alternative.
In Canvas Apps, this means every Image control needs a meaningful AccessibleLabel. Don't write "logo image" — write what the image communicates. For a status indicator image showing a green checkmark, the label should be "Request approved" not "green checkmark icon." For purely decorative images, set AccessibleLabel to empty string ("") so screen readers skip them.
// On your Image control
AccessibleLabel: If(
ThisItem.Status = "Approved",
"Request approved",
If(
ThisItem.Status = "Rejected",
"Request rejected — " & ThisItem.RejectionReason,
"Request pending review"
)
)
Notice the pattern: the label communicates meaning, not appearance, and it includes relevant context (the rejection reason) that a sighted user would see adjacent to the icon.
1.3.1 — Info and Relationships (Level A): Structure conveyed visually must be available to assistive technology.
This is where Canvas Apps struggle most. When you build a "form" by placing Labels next to Input controls, you're creating a visual association that has no semantic backing. A screen reader user tabs to the input and hears "Edit text" — not the label. You fix this through the AccessibleLabel property on every input control:
// On your TextInput for employee name
AccessibleLabel: "Employee full name, required"
HintText: "Enter first and last name"
The HintText appears visually as placeholder text and is also read by some screen readers as an additional description, but it disappears when the user starts typing. The AccessibleLabel persists as the control's name throughout the interaction. Never rely solely on HintText as the accessible name.
For groupings of related controls (like a "Billing Address" section), Power Apps doesn't have a native fieldset/legend equivalent. Your best workaround is using the AccessibleLabel on each control to make the group membership explicit:
// On the billing street address input
AccessibleLabel: "Billing address — street address, required"
// On the billing city input
AccessibleLabel: "Billing address — city, required"
1.4.3 — Contrast (Level AA): Text must have a 4.5:1 contrast ratio against its background (3:1 for large text over 18px or 14px bold).
This is the most commonly failed criterion and the easiest to measure. Power Apps Studio has a built-in color picker that shows hex values, but it doesn't calculate contrast ratios. Use the WebAIM Contrast Checker (external tool) or Colour Contrast Analyser during your design phase.
Common contrast failures in enterprise apps:
#999999 on white is 2.85:1 — fails)For disabled controls, WCAG allows an exception — disabled elements don't require contrast. But this only applies to truly non-interactive controls. If users might need to read the content to understand why it's disabled, you have a design problem, not just an accessibility problem.
1.4.11 — Non-text Contrast (Level AA): UI components (like input borders, focus indicators) must have 3:1 contrast against adjacent colors.
That light gray border on your text input? Check it. #CCCCCC on white is 1.6:1 — a significant failure. Power Apps' default input styles are often contrast failures. Override the BorderColor property explicitly:
// On TextInput controls — ensure visible border
BorderColor: RGBA(96, 96, 96, 1) // #606060 on white = 5.74:1
BorderThickness: 1
FocusedBorderColor: RGBA(0, 90, 158, 1) // Clear focus indicator
FocusedBorderThickness: 2
2.1.1 — Keyboard (Level A): All functionality must be available via keyboard.
This is the criterion that causes the most failures in Canvas Apps. Fortunately, most native Power Apps controls are keyboard-accessible by default — buttons receive Enter/Space to activate, inputs receive focus, etc. The failures usually come from custom implementations.
The most common keyboard failure pattern: Using an Image or Label control as a clickable element. Both controls support the OnSelect property and can be tapped, but neither receives keyboard focus. If someone tabs through your app, they skip those "clickable" areas entirely.
The fix: never use Image or Label controls as interactive elements. Use Button controls styled to look like whatever you need, or use the Role property on certain controls.
2.4.3 — Focus Order (Level A): The navigation order must be logical and follow the reading order.
Canvas Apps determines tab order using the TabIndex property. The default is 0 for all controls, with tab order falling back to the order controls were added to the screen — which is almost never the visual reading order in a complex app. You have two strategies:
Strategy A — Explicit TabIndex assignment: Set unique TabIndex values across your entire screen. A form with 10 fields might use 10, 20, 30... (increments of 10 leave room to insert fields later).
// Header section
HeaderLabel.TabIndex = -1 // Non-interactive, skip in tab order
// Form section
EmployeeNameInput.TabIndex = 10
DepartmentDropdown.TabIndex = 20
StartDatePicker.TabIndex = 30
ManagerLookup.TabIndex = 40
// Action section
SaveButton.TabIndex = 50
CancelButton.TabIndex = 60
Strategy B — Use TabIndex = -1 for non-interactive elements: Rather than ordering everything, set TabIndex = -1 on all purely visual elements (labels, decorative images, separator lines) so they're skipped entirely. Interactive controls keep their default TabIndex = 0, and you only explicitly set TabIndex when you need a specific ordering override.
Strategy B is lower maintenance but less precise. For complex screens with multiple sections, Strategy A gives you reliable, auditable control.
2.4.7 — Focus Visible (Level AA): Keyboard focus must be visible.
Power Apps' default focus indicators are thin and low-contrast in many themes. You must set FocusedBorderColor and FocusedBorderThickness on every interactive control. Do this in your control properties or, better, in your component templates:
// Apply to all interactive controls
FocusedBorderColor: RGBA(0, 90, 158, 1) // 4.5:1+ against white
FocusedBorderThickness: 3
For Button controls, also consider the PressedColor and HoverColor to ensure state changes are visible without relying solely on color.
Warning: The
FocusedBorderColoronly shows when the control has keyboard focus, not mouse hover focus. Test keyboard navigation separately from mouse navigation — they can behave very differently.
Most tutorials stop at AccessibleLabel and consider the job done. Real screen reader support is significantly more complex. Let's address the patterns that actually make the difference between an "accessible" app and a usable one.
When your app dynamically updates content — search results appearing, validation errors, success messages, loading indicators — screen readers don't announce those changes unless you specifically engineer them to do so.
Canvas Apps doesn't expose ARIA live region properties directly. The workaround is using a hidden Label control that you update whenever you need to announce something, positioned off-screen (or sized to 1x1 pixel with overflow hidden) but still rendered in the DOM.
Here's the pattern:
First, create a context variable to hold your announcement text:
// This formula goes wherever you trigger the announcement
// For example, in a button's OnSelect:
UpdateContext({
varAnnouncement: "Search complete. " & CountRows(colFilteredResults) & " results found."
});
// After a brief delay, if needed, clear it so the same message can be announced again
UpdateContext({varAnnouncement: ""})
Then create a Label control (lblAnnouncement) with these properties:
Text: varAnnouncement
Visible: true
Width: 1
Height: 1
X: -9999 // Off-screen
Y: -9999
AccessibleLabel: varAnnouncement
Color: Transparent
Important nuance: Setting
Visible: falseremoves the control from the accessibility tree entirely, so screen readers can't read it. Setting it off-screen (or 1x1 pixels) keeps it in the DOM and readable. This is intentional — it mirrors the standard CSSvisually-hiddentechnique used in web accessibility.
For this pattern to trigger a new announcement, the Text property must actually change. If you need to announce the same message twice, clear the variable first and then set it again — you can chain this with timers or use a Toggle control as a trigger.
When a toggle switches, a step wizard advances, or a panel opens, screen readers need to announce what changed. Power Apps has some built-in support here through the LiveRegion equivalent behavior of certain controls, but it's inconsistent.
For a multi-step form wizard, don't just change which controls are visible. Update a status announcement:
// When moving to step 2
OnSelect:
UpdateContext({
varCurrentStep: 2,
varAnnouncement: "Step 2 of 4: Employment Details. Complete the fields below."
})
One of the hardest accessibility problems in Canvas Apps is modal dialogs. When you show a "modal" (usually implemented as a Container or set of controls that appear over the main content), keyboard and screen reader users need:
Canvas Apps doesn't have native focus management APIs. You can't programmatically set focus to a specific control using Power Fx. This is a significant limitation, and honest documentation would call it a gap.
Workaround approach: The closest approximation is a combination of:
SetFocus() — this Power Fx function was added to allow programmatic focus control. Use it when a modal opens to move focus to the first element.// Button that opens the modal
OnSelect:
UpdateContext({varShowDeleteModal: true});
SetFocus(btnModalConfirm)
Managing tab order so that the modal controls have TabIndex values that come after everything on the main screen. This doesn't create a true focus trap (users can still tab out), but it minimizes wandering.
Setting TabIndex = -1 on all background controls when the modal is visible:
// On background controls' TabIndex property
If(varShowDeleteModal, -1, 10) // Remove from tab order when modal is open
This last technique is the closest to a true focus trap. The computation overhead is worth it for critical workflows.
SetFocus() to return focus to the triggering button:// Modal cancel button
OnSelect:
UpdateContext({varShowDeleteModal: false});
SetFocus(btnOpenDeleteModal)
SetFocus() gotcha:
SetFocus()only works on controls that are currently visible and haveTabIndex ≠ -1. If you're toggling visibility, the timing between the visibility change and theSetFocus()call can cause the focus to miss. Add a briefTimercontrol or restructure the visibility logic to ensure the target control is interactive before callingSetFocus().
For a real enterprise app — say, an expense report submission form with multiple sections, conditional fields, an attachment upload, and action buttons — tab order management becomes a genuine engineering challenge.
Here's how to approach it systematically:
Step 1: Document your intended tab order before building. Create a simple numbered list of every interactive element in the reading order you intend. Include conditional elements and note what happens to the tab order when they appear/disappear.
Step 2: Assign TabIndex values in blocks. Use blocks of 100 to give yourself room:
100-199: Header/navigation controls
200-299: Section 1 — Requester Information
300-399: Section 2 — Expense Details
400-499: Section 3 — Attachments
500-599: Conditional approval section (appears based on amount)
900-999: Action buttons (always last)
Step 3: Handle conditional controls. When a field only appears under certain conditions, its TabIndex should be either its assigned value (when visible) or -1 (when hidden):
// On the conditional "Project Code" field
TabIndex: If(varExpenseType = "Project", 350, -1)
Step 4: Verify with keyboard testing. Tab through the entire form using only a keyboard before considering it done. You'll almost certainly find surprises.
WCAG 2.1 Success Criterion 2.1.4 (Level A in 2.1) recommends that single character key shortcuts be dismissible, remappable, or only active on focus. For enterprise apps used daily by power users, keyboard shortcuts significantly improve efficiency for users with motor disabilities.
Canvas Apps supports keyboard shortcuts through the OnKeyDown property available on some controls and through the global App.OnKeyDown behavior:
// App.OnKeyDown - global keyboard shortcut handler
Switch(
Lower(Key),
"s", // Ctrl+S equivalent pattern
If(
Keyboard.CtrlKey,
SubmitForm(formExpenseReport);
Notify("Expense report saved", NotificationType.Success)
),
"escape",
If(
varShowModal,
UpdateContext({varShowModal: false});
SetFocus(btnTrigger)
)
)
Important: The
App.OnKeyDownevent captures keys globally, which can conflict with browser shortcuts and screen reader shortcuts. Be selective — only implement shortcuts for your most critical actions, and document them prominently. WCAG requires that users can turn off or remap single-key shortcuts if they conflict with assistive technology commands.
Implement a keyboard shortcuts reference accessible from a ? button or keyboard icon:
// Keyboard shortcuts modal content
"Keyboard Shortcuts:" & Char(10) &
"Ctrl+S — Save draft" & Char(10) &
"Ctrl+Enter — Submit form" & Char(10) &
"Escape — Close dialog / Cancel" & Char(10) &
"Ctrl+/ — Show this help"
Galleries are the most widely used Canvas App control for displaying tabular data, and they're one of the hardest to make genuinely accessible. The fundamental problem is that a Gallery renders as a list of identical containers — it doesn't generate <table>, <tr>, <th>, <td> semantics that screen readers understand as a data table.
For a gallery displaying expense line items, you need to make the semantic relationships explicit through accessible labels:
// On the container/template within the gallery
AccessibleLabel: "Expense item " & CountRows(Filter(colExpenses, ID < ThisItem.ID)) + 1 &
" of " & CountRows(colExpenses) &
": " & ThisItem.Description &
", " & Text(ThisItem.Amount, "[$-en-US]$#,##0.00") &
", " & ThisItem.Category &
", submitted " & Text(ThisItem.Date, "mmmm d, yyyy") &
". Status: " & ThisItem.ApprovalStatus
This label tells a screen reader user everything they need to know about that record: its position in the list, the key data fields, and the status. It's verbose, but screen reader users can adjust their verbosity settings — what you can't do is provide too little information.
For the interactive elements within each gallery row, use context-aware labels:
// Edit button within gallery template
AccessibleLabel: "Edit expense: " & ThisItem.Description &
", " & Text(ThisItem.Amount, "[$-en-US]$#,##0.00")
// Delete button within gallery template
AccessibleLabel: "Delete expense: " & ThisItem.Description
Never label these as just "Edit" or "Delete" — when a screen reader announces the buttons as the user tabs through, they'll hear "Edit, Edit, Edit, Delete, Delete, Delete" with no way to know which record each button affects.
If your gallery is visually formatted as a table with column headers above it, you need to handle the headers carefully. The column header labels themselves should have TabIndex = -1 (they're not interactive), and the column structure should be communicated through the row labels:
// In the gallery template's AccessibleLabel
"Row " & CountRows(Filter(colData, ID < ThisItem.ID)) + 1 &
": Description: " & ThisItem.Description &
", Amount: " & Text(ThisItem.Amount, "[$-en-US]$#,##0.00") &
", Category: " & ThisItem.Category
By repeating the column name before each value in the row label, you give screen reader users the same mental model that sighted users get from visual column headers.
If you implement sortable columns (common in enterprise data apps), the sort button must communicate its current state:
// Sort button for the "Amount" column
AccessibleLabel: "Sort by Amount, " &
If(
varSortColumn = "Amount" && varSortAscending,
"currently sorted ascending",
If(
varSortColumn = "Amount" && !varSortAscending,
"currently sorted descending",
"not sorted"
)
)
Validation errors are a frequent source of accessibility failures. A sighted user sees a red border and an error message appear next to the field. A screen reader user submits the form, nothing happens, and they have no idea why.
Step 1: Announce that validation failed. When the user attempts to submit and validation fails, immediately update your announcement variable:
// Submit button OnSelect
If(
// Validation logic
IsBlank(txtEmployeeName.Text) || IsBlank(dpStartDate.SelectedDate),
// Validation failed
UpdateContext({
varSubmitAttempted: true,
varAnnouncement: "Form submission failed. " &
Text(CountIf([
IsBlank(txtEmployeeName.Text),
IsBlank(dpStartDate.SelectedDate)
], Value)) &
" required fields are incomplete. Please review highlighted fields."
}),
// Validation passed
SubmitForm(formEmployee);
UpdateContext({varAnnouncement: "Employee record submitted successfully."})
)
Step 2: Mark individual fields as invalid. Power Apps' TextInput control supports Mode: TextInputMode.SingleLine and similar configuration, but doesn't expose an aria-invalid attribute directly. The workaround is using the AccessibleLabel to communicate the invalid state:
// On the Employee Name input
AccessibleLabel: "Employee full name, required" &
If(
varSubmitAttempted && IsBlank(txtEmployeeName.Text),
", error: this field is required",
""
)
Step 3: Make error messages accessible. Display inline error messages as Label controls beneath the input, and ensure they're included in the tab order or in the accessible label:
// Error label beneath the input
Visible: varSubmitAttempted && IsBlank(txtEmployeeName.Text)
Text: "Employee name is required"
Color: RGBA(164, 0, 0, 1) // Dark red — sufficient contrast
Role: LabelRole.Heading // Makes it more prominent in accessibility tree
TabIndex: -1 // Non-interactive, skip in tab order
Step 4: Move focus to the first invalid field after a failed submission:
// After validation failure
If(
varSubmitAttempted && IsBlank(txtEmployeeName.Text),
SetFocus(txtEmployeeName),
If(
varSubmitAttempted && IsBlank(dpStartDate.SelectedDate),
SetFocus(dpStartDate)
)
)
Power Apps Studio includes an Accessibility Checker (available under the App menu → Accessibility). Use it, but understand what it can and cannot catch.
What the Accessibility Checker finds:
AccessibleLabel on interactive controlsAccessibleLabel valuesTabIndex that might create issuesWhat it misses (the important stuff):
Rule of thumb: A green Accessibility Checker report means you've passed the minimum automated checks — it does not mean your app is accessible. Always supplement with manual testing using real assistive technology.
Windows + NVDA (recommended for enterprise):
NVDA + space to enter Forms Mode for form fieldsmacOS + VoiceOver:
Cmd + F5Ctrl + Option + arrows to navigateCtrl + Option + Space to activate controlsDocument your testing results in a spreadsheet: control name, what the screen reader announced, expected announcement, pass/fail. This becomes your compliance audit trail.
Let's build a section of an expense report form that implements everything we've covered. This is a fragment — a complete form would have more fields — but it demonstrates all the key patterns.
Setup: Create a new Canvas App (tablet layout). Enable "Improved accessibility" in Settings.
Step 1: Create context variables. In the App.OnStart property:
UpdateContext({
varCurrentStep: 1,
varSubmitAttempted: false,
varAnnouncement: "",
varShowDeleteModal: false,
varSelectedExpenseId: ""
})
Step 2: Add the announcement label. Place a Label control anywhere on the screen:
Name: lblAnnouncement
Text: varAnnouncement
X: -9999
Y: -9999
Width: 1
Height: 1
Visible: true
AccessibleLabel: varAnnouncement
Color: Transparent
TabIndex: -1
Step 3: Create the expense description input with full accessibility:
Add a TextInput control:
Name: txtExpenseDescription
AccessibleLabel: "Expense description, required" &
If(varSubmitAttempted && IsBlank(txtExpenseDescription.Text),
", error: description is required",
"")
HintText: "e.g., Client dinner — Acme Corp"
TabIndex: 100
FocusedBorderColor: RGBA(0, 90, 158, 1)
FocusedBorderThickness: 3
BorderColor: RGBA(96, 96, 96, 1)
BorderThickness: 1
Add a Label below it for inline error:
Name: lblDescriptionError
Text: "Expense description is required"
Visible: varSubmitAttempted && IsBlank(txtExpenseDescription.Text)
Color: RGBA(164, 0, 0, 1)
TabIndex: -1
Step 4: Add the amount input with format validation:
Name: txtExpenseAmount
AccessibleLabel: "Amount in US dollars, required. Enter numbers only." &
If(varSubmitAttempted && IsBlank(txtExpenseAmount.Text),
", error: amount is required",
If(varSubmitAttempted && !IsMatch(txtExpenseAmount.Text, "^\d+(\.\d{1,2})?$"),
", error: enter a valid dollar amount",
""))
TabIndex: 110
FocusedBorderColor: RGBA(0, 90, 158, 1)
FocusedBorderThickness: 3
Step 5: Build the submit button with announcement on action:
Name: btnSubmitExpense
Text: "Submit Expense Report"
AccessibleLabel: "Submit expense report"
TabIndex: 900
OnSelect:
If(
IsBlank(txtExpenseDescription.Text) ||
IsBlank(txtExpenseAmount.Text) ||
!IsMatch(txtExpenseAmount.Text, "^\d+(\.\d{1,2})?$"),
// Validation failed
UpdateContext({
varSubmitAttempted: true,
varAnnouncement: "Submission failed: please correct the errors in the form."
});
If(IsBlank(txtExpenseDescription.Text),
SetFocus(txtExpenseDescription),
SetFocus(txtExpenseAmount)
),
// Validation passed — submit
Patch(
ExpenseReports,
Defaults(ExpenseReports),
{
Description: txtExpenseDescription.Text,
Amount: Value(txtExpenseAmount.Text),
SubmittedBy: User().Email,
SubmittedDate: Today()
}
);
UpdateContext({
varAnnouncement: "Expense report submitted successfully. Your reference number is " &
Last(ExpenseReports).ID & "."
});
Reset(txtExpenseDescription);
Reset(txtExpenseAmount);
UpdateContext({varSubmitAttempted: false})
)
Step 6: Test keyboard navigation.
Step 7: Test with NVDA.
Mistake 1: Using Visible: false for elements that need to be read.
When Visible: false, the control is completely removed from the accessibility tree. For your announcement label, loading indicators that should be announced, or error summaries, use the off-screen positioning technique instead.
Mistake 2: Relying on HintText as the accessible name.
HintText renders as a placeholder that disappears when the user types. Some screen readers also stop reading it once the field has content. Always set AccessibleLabel.
Mistake 3: Setting TabIndex = 0 on everything and assuming it will work.
TabIndex = 0 puts the control in the "natural" tab order, which in Canvas Apps is determined by the order controls were created — not their visual position. After any screen reorganization, your tab order is likely scrambled. Use explicit positive TabIndex values for any screen with more than five interactive elements.
Mistake 4: Not testing with the real app, not the Studio preview.
Canvas Apps behaves differently in Studio preview versus the published player. Keyboard behavior, focus management, and ARIA announcements can all differ. Always test accessibility in the published app using the actual Power Apps player URL.
Mistake 5: Color-only state indicators.
If your approval status indicator only uses green/red color to show approved/rejected, you're failing WCAG 1.4.1 (Use of Color). Add text, icons, or patterns in addition to color. Your accessible labels handle the screen reader case, but colorblind users also need a non-color differentiator in the visual display.
Mistake 6: Forgetting that Galleries reset tab order.
Each item in a Gallery shares the same template, so the TabIndex values within the template are relative, not global. Tab order through a gallery works vertically (all items in one row, then next row) in vertical galleries, and horizontally in horizontal galleries. This behavior can surprise users who expect a specific reading order. Test it explicitly.
Troubleshooting: SetFocus() not working.
If SetFocus() isn't moving focus to the expected control, check:
Visible: true)?TabIndex ≠ -1 on the target control?Visible in the same formula, try triggering SetFocus() from a separate step or Timer control.SetFocus() may not cross component boundaries in all cases.Troubleshooting: Screen reader announcing wrong content.
If your screen reader announces the control name (like "Button1") instead of your AccessibleLabel, the AccessibleLabel property may be blank or the property may not have been saved. Check the property in Studio and republish. Also verify you're testing the published app, not the preview.
Troubleshooting: Announcement variable not triggering re-announcement.
If you update varAnnouncement to the same string it already holds, the DOM doesn't change and the screen reader doesn't re-announce. Clear the variable first (UpdateContext({varAnnouncement: ""})), then set it to the new value. Use a Timer control if you need a brief delay between the clear and the set.
There's a common concern that accessibility properties add performance overhead. The reality: properly implemented accessibility has negligible performance impact. The patterns that do introduce overhead — excessive If() calculations in AccessibleLabel properties that run constantly — are more an efficiency concern than an accessibility concern, and the same optimization advice applies regardless.
What actually matters for performance in accessible apps:
varAnnouncement in loops or in frequently-recalculating properties. Update it only on discrete user actions.If(varShowModal, -1, 10) on every background control when a modal opens means dozens of property recalculations. This is fine for small screens; for screens with 50+ controls, test responsiveness when opening modals.Accessibility in Canvas Apps is a genuine engineering discipline, not a checklist. The apps that truly work for users with disabilities — and that survive compliance audits — are the ones built with accessibility as a first-class requirement alongside functionality and performance.
Here's what you've covered in this lesson:
AccessibleLabel, TabIndex, FocusedBorderColor, contrast ratios, and keyboard activation.SetFocus() for focus management.TabIndex assignment in logical blocks, conditional removal of background controls during modals, and programmatic focus management via SetFocus().Your immediate next steps:
From here, explore Power Apps component framework (PCF) custom controls for scenarios where Canvas App native controls genuinely can't meet accessibility requirements — PCF components let you write HTML/JavaScript that can expose proper ARIA semantics. Also investigate Power Apps' integration with Microsoft Accessibility Insights for automated accessibility scanning of your published apps at scale.
Accessibility is also a forcing function for better design overall. Apps with logical tab order, clear labels, and predictable behavior are just better apps — for everyone.