The ArcGIS Experience Builder framework, component libraries, and design patterns provided in the developer edition are built to support compliance with WCAG 2.1 , Section 508 , and assistive technology requirements. This topic is intended for developers building custom widgets and components using the developer edition.
Key accessibility principles
The Experience Builder framework and its component libraries provide a strong accessibility foundation. Custom widget developers are responsible for following established best practices to ensure accessible behavior when extending the framework. When building custom widgets that integrate seamlessly into accessible Experience Builder applications, keep the following principles in mind.
-
Use Jimu UI and Calcite Components for UI elements whenever possible.
-
Respect keyboard and focus patterns.
-
Apply semantic HTML first.
Accessibility support in Experience Builder
Experience Builder framework handles many accessibility concerns automatically, including:
- Keyboard navigation across pages, layouts, and widgets.
- Screen reader–friendly page structure.
- Logical focus order based on layout and DOM order.
- Accessible window, panel, and dialog behavior.
- Theme-based color contrast and focus indicators.
Component libraries
Experience Builder contains three primary component systems that shape accessible custom widgets. Using these libraries is the most effective way to meet accessibility requirements.
Jimu UI
Jimu UI is the native React component library in Experience Builder. This library is the default library that ships with the developer edition of Experience Builder.
Jimu UI supports accessibility through:
- Built with semantic HTML.
- Keyboard interactions implemented by default.
- Focus states and tab order managed internally.
- Proper ARIA roles, states, and properties applied.
- Designed to work consistently with screen readers.
import { Button } from 'jimu-ui';
export function SubmitButton() {
return (
<Button type="primary" onClick={handleSubmit}>
Submit
</Button>
);
}Calcite Components
Calcite Components are framework-agnostic, accessible web components used across the ArcGIS products.
Calcite Components supports accessibility through:
- WCAG-aligned color contrast and focus indicators.
- Keyboard navigation built in.
- Screen reader announcements handled internally.
- ARIA Authoring Practices followed for complex widgets.
Calcite Components are ideal when building advanced UI such as dialogs, dropdowns, or panels.
import '@esri/calcite-components/dist/components/calcite-dialog';
export function ConfirmDialog() {
return (
<calcite-dialog open heading="Confirm delete">
Are you sure you want to delete this item?
</calcite-dialog>
);
}ArcGIS Maps SDK for JavaScript components
You can create map-centric widgets using the ArcGIS Maps SDK for JavaScript components in Experience Builder. This SDK provides a variety of UI components for building map interactions. The Maps SDK components foundationally are built on Calcite Components, and are designed with accessibility in mind.
The Maps SDK components provide support for accessibility such as:
- Map UI components expose keyboard support where applicable.
- Popups, feature tables, and editors follow accessible patterns.
- Screen reader announcements are supported for non-visual interaction.
- ARIA roles and states are applied as needed.
<!-- Load Calcite components and the JavaScript Maps SDK core API-->
<script
type="module"
src="https://js.arcgis.com/calcite-components/3.3.3/calcite.esm.js"></script>
<link rel="stylesheet" href="https://js.arcgis.com/4.34/esri/themes/dark/main.css" />
<script src="https://js.arcgis.com/4.34/"></script>
<!-- Load Map components -->
<script
type="module"
src="https://js.arcgis.com/4.34/map-components/"
></script>
...
<arcgis-map item-id="05e015c5f0314db9a487a9b46cb37eca"></arcgis-map>Best practices
When building custom widgets, follow these best practices to ensure accessibility compliance.
Semantic HTML
Semantic HTML is the foundation of accessibility. The recommended approach is to use native elements over generic containers. Native elements provide:
- Built-in keyboard support.
- Correct roles and states.
- Predictable behavior across assistive technologies.
<!-- Avoid -->
<div onClick={onClick}>Save</div>
<!-- Prefer -->
<button onClick={onClick}>Save</button>Keyboard and focus management
All widget functionality must be operable using a keyboard. The key principles are:
- Do not trap keyboard focus unintentionally.
- Maintain focus after UI updates.
- Show visible focus indicators.
- Avoid positive
tabindexvalues.
<button
ref={buttonRef}
onClick={() => {
doAction();
buttonRef.current?.focus();
}}
>
Apply
</button>Managing composite components
For components such as lists, menus, or tab panels:
- Only one element should be in the tab order.
- Use arrow keys for internal navigation.
- Apply roving tabindex or
aria-activedescendant.
<ul role="listbox" aria-label="Options">
<li role="option" aria-selected="true">Option A</li>
<li role="option">Option B</li>
</ul>ARIA usage
Use semantic HTML as the first and preferred approach. When native elements cannot fully express the required roles, states, or relationships, use ARIA to supplement but never replace semantic HTML entirely. Follow these guidelines:
Common use cases
- Providing accessible names when no visible label exists.
- Communicating expanded or collapsed state.
- Announcing dynamic updates.
<button
aria-expanded={isOpen}
aria-controls="panel-1"
>
Filters
</button>ARIA best practices
It is recommended to limit the use of ARIA attributes to necessary cases to ensure optimal accessibility. Follow these guidelines:
- Do not override native semantics unnecessarily.
- All interactive elements must be keyboard accessible.
- Never hide visible, focusable elements with
aria-hidden. - Ensure every interactive element has an accessible name.
Text, labels, and alternatives
Clear and concise text improves usability for all users. Topics below outline key considerations.
Labels for form controls
Every input must have an accessible label.
<label for="search">Search</label>
<input id="search" type="text" />If a visible label is not possible:
<input
type="text"
aria-label="Search locations"
/>Images and icons
When using images or icons:
- Informative images require meaningful
alttext. - Decorative images must use empty alt attributes.
<!-- Informative image -->
<img src="warning.svg" alt="This is a warning icon displaying a warning message" />
<!-- Decorative image -->
<img src="divider.svg" alt="" />Color and contrast
Experience Builder themes are designed to meet contrast requirements, but custom styling must also comply.
Below are requirements from WCAG 2.1:
- Text contrast ≥ 4.5:1.
- UI component contrast ≥ 3:1.
- Do not rely on color alone to convey meaning.
Here is an example of sufficient contrast:
/* Ensure sufficient contrast */
.button {
background-color: #005a9c; /* Dark blue */
color: #ffffff; /* White */
}Testing and auditing
Automated tools catch only a portion of accessibility issues. It is recommended to use a combination of approaches:
Recommended tools
- JAWS Screen Reader
- NVDA Screen Reader
- Browser Accessibility Inspector
- axe DevTools
- Lighthouse
- eslint-plugin-jsx-a11y
- Keyboard-only testing
- Screen reader testing (NVDA, VoiceOver)
Developer checklist
Before shipping a widget, confirm:
All functionality works with keyboard only.
Focus order is logical and visible.
Accessible names are present.
ARIA roles and states are correct.
No keyboard traps exist.
Code examples
Create an accessible action button
This example shows how to create an accessible button using the Jimu UI component library:
- Uses semantic
<buttoninternally.> - Keyboard activation (
Enter,Space) supported automatically. - Focus indicator provided by theme.
- Accessible name derived from button text.
- No ARIA required.
Jimu UI handles keyboard, focus, and ARIA internally, and Experience Builder themes ensure contrast and focus visibility.
import { Button } from 'jimu-ui';
export function ApplyButton({ onApply }: { onApply: () => void }) {
return (
<Button
type="primary"
onClick={onApply}
>
Apply
</Button>
);
}Create an accessible icon-only button
This example demonstrates an accessible icon-only button using Jimu UI:
aria-labelprovides an accessible name- Icon is decorative and not announced
- Button remains keyboard operable
import { Button, Icon } from 'jimu-ui';
export function CloseButton({ onClose }: { onClose: () => void }) {
return (
<Button
icon
aria-label="Close panel"
onClick={onClose}>
<Icon icon="close" />
</Button>
);
}Create an accessible panel
This example shows how to build an accessible panel with a form using Calcite Components. It includes:
-
Labels and names
- Use
Calciteto provide a visible label for the selectLabel - Screen readers announce the control and its purpose
- No extra
aria-labeloraria-labelledbyneeded when using wrapped components
- Use
-
Keyboard support
Tabmoves between controls- Arrow keys navigate options
Enteractivates buttons- Focus order is logical and predictable
-
Focus visibility
- Calcite applies theme-consistent focus indicators
- No custom CSS required
- Meets WCAG focus visibility requirements
-
Semantics and roles
- Correct roles are applied internally
- States (expanded, selected) are communicated automatically
- No risk of incorrect ARIA usage
import { useState } from 'react';
import {
CalcitePanel,
CalciteSelect,
CalciteOption,
CalciteButton,
CalciteLabel
} from '@esri/calcite-components-react';
export function LayerFilterPanel() {
const [layer, setLayer] = useState('parcels');
return (
<CalcitePanel heading="Layer filters" description="Select a layer and apply filters">
<CalciteLabel>
Select layer
<CalciteSelect
value={layer}
onCalciteSelectChange={(event: any) => setLayer(event.target.value)}
>
<CalciteOption value="parcels">Parcels</CalciteOption>
<CalciteOption value="buildings">Buildings</CalciteOption>
<CalciteOption value="roads">Roads</CalciteOption>
</CalciteSelect>
</CalciteLabel>
<CalciteButton appearance="solid" width="full" onClick={() => applyFilter(layer)}>
Apply filters
</CalciteButton>
</CalcitePanel>
);
}Create an accessible map with updates and feature details
This example demonstrates how to create an accessible map component using the ArcGIS Maps SDK for JavaScript components. The example:
-
Uses official map component patterns
<arcgis-mapcan contain other components and connect them via slots.> arcgis/View Ready Change viewis the recommended readiness signal before touching map/view state.On Ready()
-
Uses SDK-supported ARIA for the view container
- Apply ARIA attributes on the map container (e.g.,
aria-label, optionallyaria-describedby) to provide an accessible name and description.
- Apply ARIA attributes on the map container (e.g.,
-
Provides better screen reader experience with features outside the map
<arcgis-featuresprovides popup-style content in a separate container, which is ideal for screen readers and keyboard users.>
import "@arcgis/map-components/components/arcgis-map";
import "@arcgis/map-components/components/arcgis-search";
import "@arcgis/map-components/components/arcgis-zoom";
import "@arcgis/map-components/components/arcgis-legend";
import "@arcgis/map-components/components/arcgis-feature";
import React, { useEffect, useRef, useState } from "react";
export function AccessibleMapWithDetails() {
const mapElRef = useRef<HTMLArcgisMapElement | null>(null);
const [status, setStatus] = useState("Loading map…");
useEffect(() => {
const mapEl = mapElRef.current;
if (!mapEl) return;
const onViewReady = async () => {
// Ensure view is ready before interacting (best practice with components)
await mapEl.viewOnReady();
// Provide a concise accessible name for assistive technologies
mapEl.setAttribute("aria-label", "Interactive web map");
setStatus("Map loaded. Use the Search control to find a place.");
};