Accessibility & ARIA

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against GWT 2.13.1 (released 2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, and the Jakarta Servlet 6 server of the Bookshelf scenario, checked on 2026-10-08. Accessibility in GWT is client knowledge: the ARIA library is a package of gwt-user (module com.google.gwt.aria.Aria, inherited by com.google.gwt.user.User) that the compiler turns into JavaScript with your code. Nothing is shared with the server and the build needs no extra dependency. Every example was compiled with the 2.13.1 compiler and loaded in headless Chrome (driven by dispatching keydown events on the focused element); the attribute dumps, compiler messages and focus results quoted below are what the browser and javac printed. The general theory (what accessibility is, WCAG 2.2, legal standards, validators) is not repeated here; it is in Web Accessibility, and this page is the GWT-specific part.

What the official guide says, and what changed

The Developer Guide page on accessibility is short and has one idea: a widget built from div and span elements is just text to a screen reader, so a widget author must (a) tell the browser what the element is (a role), (b) keep what it is doing up to date (states and properties), and (c) make sure keyboard users can reach it and that focus lands where the screen reader will announce it. GWT gives you a typed Java API for the attributes (this page’s first half) and nothing magic for the other two: focus and keyboard behaviour are ordinary event handling, which the second half covers with runnable code.

Table 1. Where accessibility support came from
Release Change

1.0.21

The release notes announce an Accessibility class with which widget authors add ARIA support. This is the class that is now deprecated.

2.5.0 (RC1)

"A new accessibility library for setting ARIA roles, states and properties on DOM elements" (com.google.gwt.aria.client). Many built-in widgets gained roles.

2.5.0 (final)

"The accessibility library introduced in RC1 has been cleaned up for release."

2.13.1

Same library; the Accessibility class is still in gwt-user and still @Deprecated.

The 2007 GWT in Action book predates ARIA and does not mention it; treat the book as a source for widget concepts, not for accessibility (it is listed in the references).

The ARIA library at a glance

The package com.google.gwt.aria.client mirrors the WAI-ARIA 1.0 vocabulary as Java types. Its pieces:

Table 2. The types of com.google.gwt.aria.client
Type What it is

Roles

A final factory with one static getter per concrete role (getButtonRole(), getTreeitemRole(), …​): 61 of them in 2.13.1, plus roleOf(Element), which reads the role attribute and returns the matching Role or null.

Role and the role interfaces

Role holds the methods every element can have (set, remove, getName, name/description/live/hidden/disabled/invalid accessors and the tabindex accessor). The roles form a hierarchy of Java interfaces that mirrors the ARIA role model, so ButtonRole extends CommandRole, TreeRole extends SelectRole, and so on. Only the concrete roles that Roles returns are meant to be applied to elements.

State, Property, ExtraAttribute

Static constants of type Attribute<T>: State.PRESSED, Property.LABEL, ExtraAttribute.TABINDEX. Each knows the real attribute name (aria-pressed) and how to write a value of type T.

Value types

Enums for the token attributes (PressedValue, CheckedValue, ExpandedValue, SelectedValue, InvalidValue, LiveValue, OrientationValue, SortValue, AutocompleteValue, DropeffectValue, GrabbedValue, RelevantValue) and Id for references to other elements. Plain boolean, int, Number and String are used where the ARIA type is a boolean, integer, number or string.

The design choice that matters most: the accessor methods live on the role, not on the element. A button supports aria-pressed and aria-expanded, so ButtonRole has setAriaPressedState and setAriaExpandedState; it does not have setAriaCheckedState, because the ARIA role model does not allow that state on a button. The compiler therefore rejects states that make no sense for the role you picked. The official guide recommends going through the role for exactly this reason.

flowchart LR roles["Roles.getXxxRole()
(factory, one per concrete role)"] --> role["Role subtype
ButtonRole, TabRole, TreeitemRole ..."] role --> set["set(element) / remove(element)
writes the role attribute"] role --> state["setAriaXxxState(element, value)
dynamic: follows user actions"] role --> prop["setAriaXxxProperty(element, value)
rarely changes: names, levels, ranges"] role --> extra["setTabindexExtraAttribute(element, int)
the tabindex attribute"] state --> dom["DOM attribute
aria-pressed=true"] prop --> dom extra --> dom2["DOM attribute
tabindex=0"] dom --> at["Browser accessibility tree
screen reader announcement"] dom2 --> at

Role, state, property: three kinds of attribute

The guide’s distinction is worth keeping because it tells you when to write each attribute:

Table 3. Roles, states and properties
Kind Lifetime When to write it

Role

Static: the widget’s type does not change.

Once, in the constructor: Roles.getButtonRole().set(getElement()). One role per element; setting a second role overwrites the first (see the pitfalls table).

State

Dynamic: aria-busy, aria-checked, aria-disabled, aria-expanded, aria-grabbed, aria-hidden, aria-invalid, aria-pressed, aria-selected.

Initialise at construction and update in every handler that changes how the widget looks. The Javadoc of Roles says to update ARIA attributes at the same time as the CSS style, so that the screen reader sees what a sighted user sees.

Property

Changes rarely or never: labels, levels, ranges, relationships.

Normally once. aria-activedescendant is the usual exception because it moves with the selection.

The 2.13.1 sources define 9 states in State and 25 properties in Property (the complete list is in the sources linked in the references). Their value types are, in short:

Table 4. Value types of the most used attributes
Attribute Java accessor on the role Value type

aria-label

setAriaLabelProperty(Element, String)

String

aria-labelledby, aria-describedby

setAriaLabelledbyProperty(Element, Id...), setAriaDescribedbyProperty(Element, Id...)

one or more Id

aria-activedescendant

setAriaActivedescendantProperty(Element, Id)

one Id; only on roles that can have an active descendant (CompositeRole, GroupRole and TextboxRole, and their subtypes)

aria-pressed

setAriaPressedState(Element, PressedValue)

enum TRUE, FALSE, MIXED, UNDEFINED; PressedValue.of(boolean) is a shortcut

aria-disabled, aria-hidden, aria-required

setAriaDisabledState(Element, boolean) and similar

boolean

aria-invalid

setAriaInvalidState(Element, InvalidValue)

enum FALSE, TRUE, GRAMMAR, SPELLING (there is no of(boolean))

aria-live

setAriaLiveProperty(Element, LiveValue)

enum OFF, POLITE, ASSERTIVE

aria-valuenow, aria-valuemin, aria-valuemax, aria-valuetext

setAriaValuenowProperty(Element, Number) and so on, on RangeRole subtypes

Number / String

aria-level, aria-setsize, aria-posinset

setAriaLevelProperty(Element, int) and so on

int

tabindex

setTabindexExtraAttribute(Element, int)

int, available on every role

What the library does not cover

The library is the ARIA 1.0 vocabulary of 2011. It does not model anything that WAI-ARIA 1.1 and 1.2 added, so there is no SwitchRole, TableRole, CellRole, SearchboxRole or FeedRole, and no accessor for aria-modal, aria-current, aria-errormessage, aria-roledescription or aria-keyshortcuts. The 1.0 drag-and-drop attributes (aria-grabbed, aria-dropeffect) are in the library but are deprecated in current ARIA. For anything missing, write the attribute directly: element.setAttribute("aria-modal", "true") on a com.google.gwt.dom.client.Element is plain DOM and works with the typed API on the same element (the dialog example below does it). Prefer native HTML elements where the platform has one (table, button, dialog-like patterns), see the advice at the end of this page.

Example 1: a button made from a div

The first Bookshelf widget is an icon button that marks a book as one the reader wants to borrow. It is a div (so that it can show a glyph and be styled), which means nothing is a button unless we say so. The widget does the five things the guide lists: pick the role, set it at construction, set the accessible name, initialise the state, and update the state in the handler.

PressableIcon.java (Bookshelf client)
public class PressableIcon extends FocusPanel {
  private boolean pressed;

  public PressableIcon(String accessibleName) {
    Element el = getElement();
    el.setInnerText("☆"); // a star glyph: not a readable name on its own
    Roles.getButtonRole().set(el);                                          // role, once
    Roles.getButtonRole().setAriaLabelProperty(el, accessibleName);         // accessible name
    Roles.getButtonRole().setAriaPressedState(el, PressedValue.of(false));  // initial state
    setTabIndex(0);                                                         // in the tab order
    addClickHandler(e -> toggle());
    addKeyDownHandler(e -> {
      int k = e.getNativeKeyCode();
      if (k == KeyCodes.KEY_ENTER || k == ' ') {
        e.preventDefault();   // Space would otherwise scroll the page
        toggle();
      }
    });
  }

  void toggle() {
    pressed = !pressed;
    Roles.getButtonRole().setAriaPressedState(getElement(), PressedValue.of(pressed)); // state follows the UI
    getElement().setInnerText(pressed ? "★" : "☆");
  }
}

What the browser held for the widget, and after pressing Enter and Space on it:

DOM of PressableIcon (headless Chrome)
before: <div role="button" aria-label="Add to my loans" aria-pressed="false" tabindex="0">
Enter : <div aria-pressed="true">
Space : <div aria-pressed="false">

The div is now announced as "Add to my loans, toggle button, not pressed" instead of as an unnamed star glyph, can be reached with Tab, and answers to Enter and Space the way a button does. A real button element would have given all of that for free; the page keeps the div because that is the situation of every custom widget (the guide’s own advice is to use native controls whenever you can). GWT’s own CustomButton does the same: its constructor calls Roles.getButtonRole().set(getElement()), a private setAriaPressed writes aria-pressed whenever the button face changes, and setEnabled(false) removes aria-pressed. (The guide’s text says the widget also initialises aria-disabled; the 2.13.1 source does not write that state, which is another reason to read the source.)

Example 2: a rating slider with a spoken value

A rating from one to five stars is a range, and ARIA has a role for ranges. The values are properties on RangeRole subtypes, so they only exist on the slider role.

RatingSlider.java: role, range and text alternative
public RatingSlider() {
  Roles.getSliderRole().set(getElement());
  Roles.getSliderRole().setAriaLabelProperty(getElement(), "Your rating");
  Roles.getSliderRole().setAriaValueminProperty(getElement(), 1);
  Roles.getSliderRole().setAriaValuemaxProperty(getElement(), 5);
  setTabIndex(0);
  render();
  addKeyDownHandler(e -> {
    switch (e.getNativeKeyCode()) {
      case KeyCodes.KEY_RIGHT: case KeyCodes.KEY_UP:  set(value + 1); break;
      case KeyCodes.KEY_LEFT:  case KeyCodes.KEY_DOWN: set(value - 1); break;
      case KeyCodes.KEY_HOME:  set(1); break;
      case KeyCodes.KEY_END:   set(5); break;
      default: return;
    }
    e.preventDefault();
  });
}

private void render() {
  getElement().setInnerText("★".repeat(value) + "☆".repeat(5 - value));
  Roles.getSliderRole().setAriaValuenowProperty(getElement(), value);
  // The number alone says little: give the screen reader a word as well.
  Roles.getSliderRole().setAriaValuetextProperty(getElement(), value + " of 5, " + WORDS[value]);
}
DOM of RatingSlider after three Right presses and Home
initial    : <div role="slider" aria-valuemin="1" aria-valuemax="5" aria-valuenow="3" aria-valuetext="3 of 5, good" tabindex="0">
3x Right   : <div aria-valuenow="5" aria-valuetext="5 of 5, excellent">
Home       : <div aria-valuenow="1" aria-valuetext="1 of 5, poor">

The value stops at 5 because set clamps it. The text "3 of 5, good" is user-visible language: it has to come from your message bundle in a localised application (see Internationalization), not from a string literal as in this sketch. That is also the guide’s own objection to hand-written spoken text in hidden elements, below.

Names, descriptions and live regions

A control needs a name ("what is this?"), sometimes a description ("anything I should know?") and, when it changes without taking focus, an announcement. The ARIA properties for these are aria-label, aria-labelledby, aria-describedby and aria-live.

Labels: aria-labelledby and the Id type

A page often has a visible Label next to a widget, but the browser does not know they belong together. aria-labelledby makes the link explicit, and the target is given as an Id. An Id is created with Id.of(Element) (the element must already have an id attribute) or Id.of(String).

ReviewForm.java: a text box with a label, a hint and a polite status line
label.getElement().setId(Document.get().createUniqueId());
hint.getElement().setId(Document.get().createUniqueId());
status.getElement().setId(Document.get().createUniqueId());
Element in = title.getElement();
// Name, description and "required" are properties: set once.
Roles.getTextboxRole().setAriaLabelledbyProperty(in, Id.of(label.getElement()));
Roles.getTextboxRole().setAriaDescribedbyProperty(in, Id.of(hint.getElement()));
Roles.getTextboxRole().setAriaRequiredProperty(in, true);
// A live region: its role and politeness exist before the first message.
Roles.getStatusRole().set(status.getElement());
Roles.getStatusRole().setAriaLiveProperty(status.getElement(), LiveValue.POLITE);

public boolean validate() {
  boolean ok = !title.getText().isEmpty() && title.getText().length() <= 40;
  // Invalid is a state: it follows the validation result.
  Roles.getTextboxRole().setAriaInvalidState(title.getElement(), ok ? InvalidValue.FALSE : InvalidValue.TRUE);
  status.setText(ok ? "Review title accepted." : "Review title is required.");
  return ok;
}
DOM produced by ReviewForm
textbox : <input aria-labelledby="gwt-uid-1" aria-describedby="gwt-uid-2" aria-required="true">
status  : <div role="status" aria-live="polite">
empty   -> <input aria-invalid="true">  status="Review title is required."
valid   -> <input aria-invalid="false"> status="Review title accepted."

Document.get().createUniqueId() returns gwt-uid-N strings, so the ids cannot clash with each other or with your own. Two details that the output shows: the message is written with setText (never setHTML with text that came from a user, see SafeHtml & XSS), and the live region exists with its role and politeness before the first message, because a screen reader announces changes to a region it already knows.

aria-label is the shortcut when there is no visible text to point at (an icon button, as in example 1). It is a property, takes a String, and must be localised like any other user-visible text.

Live regions for content that moves without focus

Some interfaces change what they show without moving keyboard focus: an auto-complete list, a "3 results" counter, a "saved" confirmation. A screen reader announces the element that has focus, so these changes are silent unless they are in a live region. The guide’s example puts the suggestions in a hidden div with the region role and aria-live set to assertive, and updates its text on every key press. The LiveValue enum has OFF, POLITE and ASSERTIVE; use POLITE unless the message cannot wait, and use Roles.getStatusRole() or Roles.getAlertRole() when the role itself says how urgent the message is.

The guide also lists the weaknesses of hand-written spoken text, and they still hold: each developer picks different wording (an inconsistent experience between applications), and the words that are never shown on screen ("Suggestions") are easy to forget to translate. Whenever a role plus states say the same thing, prefer them to custom text, because translating the spoken terms is then the screen reader’s job.

Focus management

Screen readers speak what has focus, so for a widget author "accessible" mostly means "focus is where the user expects it". GWT has the pieces; the discipline is yours.

flowchart TD start["A custom widget needs keyboard access"] --> native{"Can a native button, input
or select do the job?"} native -- "yes" --> use["Use it: focus, keys and
announcements come free"] native -- "no" --> single{"Single control?"} single -- "yes" --> tab0["role + tabindex 0 +
key handler (Enter, Space)"] single -- "no, a group of items" --> group{"Do the items
hold other focusable content?"} group -- "no" --> rove["Roving tabindex:
0 on the current item, -1 on the rest,
arrows move real focus"] group -- "yes, or too many to focus" --> desc["One focusable container +
aria-activedescendant -> item id"]

Tab order: tabindex

By default only links and form controls take focus. Setting tabindex to 0 puts any element in the natural tab order; -1 takes it out of the tab order but still allows focus() from code. Two ways to set it from GWT:

  • The Focusable interface (setTabIndex(int), getTabIndex(), setFocus(boolean), setAccessKey(char)), implemented by FocusWidget (buttons, text boxes, list boxes), FocusPanel and the cell widgets.

  • The ARIA library, for an element that is not a widget: Roles.getHeadingRole().setTabindexExtraAttribute(heading.getElement(), 0) (this is the guide’s example of making a clickable heading reachable).

Every FocusWidget is focusable by default. The guide’s caveat holds: extending FocusWidget does not make a widget focusable if the base element is not a natively focusable one. For arbitrary elements wrap them in a FocusPanel, which creates a focusable div, and test in the browsers you support.

Moving focus: FocusPanel.setFocus is deferred in WebKit permutations

Focusable.setFocus(true) is not the same call for every widget. FocusPanel and FocusWidget use two different FocusImpl instances, and the WebKit/Blink implementation (FocusImplSafari, used by the user.agent value safari, which Chrome belongs to) moves focus from a zero-delay timeout, because "attempts to focus elements from within an event callback will generally be unsuccessful". A Button.setFocus(true) is immediate; a FocusPanel.setFocus(true) is not.

Focus after FocusPanel.setFocus(true), measured in Chrome
setFocus(true) returned; active=BUTTON        <- still the previous element (a Button)
60 ms later: active is the button=true         <- the FocusPanel's div, after the timeout

So code that moves focus and then reads or relies on it (document.activeElement, a test, a second focus() call) must wait for the next turn of the event loop, for example with Scheduler.get().scheduleDeferred(...) or a Timer. Calling setFocus(true) on a FocusPanel and then immediately focusing something else leaves focus on the second element, which is not the order you wrote. GWT has no Document.getActiveElement() (the compiler says The method getActiveElement() is undefined for the type Document); read it with a one-line JSNI method, /*-{ return $doc.activeElement; }-*/.

Roving tabindex for toolbars, tab bars and menus

A toolbar with ten buttons should cost one Tab press, not ten. The standard pattern: exactly one item has tabindex="0" (the current one), the others -1; the arrow keys move the 0 and the real focus. GWT’s MenuBar does this by hand: its root menu is in the tab sequence and its sub-menus are reset to -1.

RovingToolbar.java: arrows move focus inside the group
public RovingToolbar(String label, String... names) {
  Roles.getToolbarRole().set(getElement());
  Roles.getToolbarRole().setAriaLabelProperty(getElement(), label);
  for (String n : names) {
    FocusPanel b = new FocusPanel();
    b.getElement().setInnerText(n);
    Roles.getButtonRole().set(b.getElement());
    b.setTabIndex(items.isEmpty() ? 0 : -1);   // only the first item is a Tab stop
    items.add(b);
    add(b);
  }
  addDomHandler(e -> {                             // key events bubble up to the toolbar
    int k = e.getNativeKeyCode(), next = current;
    if (k == KeyCodes.KEY_RIGHT) next = (current + 1) % items.size();
    else if (k == KeyCodes.KEY_LEFT) next = (current + items.size() - 1) % items.size();
    else if (k == KeyCodes.KEY_HOME) next = 0;
    else if (k == KeyCodes.KEY_END) next = items.size() - 1;
    else return;
    e.preventDefault();
    items.get(current).setTabIndex(-1);
    current = next;
    items.get(current).setTabIndex(0);
    items.get(current).setFocus(true);
  }, KeyDownEvent.getType());
}
Result of Right, Right, Right, then Home (the items wrap around)
Right        : active=Reserve  tabindexes=Borrow=-1 Reserve=0 Review=-1
Right x2 more: active=Borrow   tabindexes=Borrow=0 Reserve=-1 Review=-1
Home         : active=Borrow

Because the FocusPanel focus call is deferred (previous section), the lab read the active element a few milliseconds after each key. A single listener on the container, instead of one per item, is also what the guide recommends for large trees: key events bubble, so the root can handle them for all items.

Many items or nested widgets: aria-activedescendant

When the items are too many to focus, or contain focusable content of their own (a tree item with a link in it), the guide’s second technique applies: one naturally focusable element holds focus, handles all the keys, and points at the current item with aria-activedescendant, whose value is the item’s id. GWT’s Tree does exactly that: it creates a hidden focusable element with role tree, gives every item a unique id and the role treeitem, and updates the property after each selection change.

Real attributes of a GWT Tree, before and after selecting the first item
Tree focusable : <div role="tree" tabindex="0">
after select   : <div role="tree" aria-activedescendant="gwt-uid-5">
treeitem       : <div class="gwt-TreeItem gwt-TreeItem-selected" role="treeitem" id="gwt-uid-5" aria-level="1" aria-setsize="1" aria-posinset="1" aria-expanded="true" aria-selected="true">

The guide documents the weakness of the technique: a screen reader may read only the text of the item that aria-activedescendant points at and ignore its role and states, which is enough for a list box but not for a tree (which level am I on?). Some screen readers interpret the item as if it had focus, some do not, so test with the ones your users have. Also note the generated markup: the second item of the lab’s tree (not yet selected) had a role and id but no aria-level yet, because Tree writes level, set size, position and expanded state when an item is selected.

Dialogs: put focus in, give it back

DialogBox and PopupPanel set no ARIA role. A modal-looking panel therefore needs three things from you: a role (dialog) with a name, focus moved inside when it opens, and focus returned to the element that opened it when it closes.

Opening and closing a dialog without losing the keyboard user
Element opener = active();                  // remember who had focus (JSNI helper above)
DialogBox dlg = new DialogBox(false, true); // autoHide=false, modal=true
dlg.setText("Loan this book");
Roles.getDialogRole().set(dlg.getElement());
Roles.getDialogRole().setAriaLabelProperty(dlg.getElement(), "Loan this book");
dlg.getElement().setAttribute("aria-modal", "true"); // not in the 2.13.1 library: plain attribute
...
dlg.center();
who.setFocus(true);                          // first field; a TextBox is a FocusWidget: immediate

close.addClickHandler(e -> { dlg.hide(); opener.focus(); });
Result
dialog open  : active is textbox=true  <div role="dialog" aria-label="Loan this book" aria-modal="true">
dialog closed: focus back on opener=true

Keyboard navigation in cell widgets

The cell widgets (CellList, CellTable, DataGrid, CellTree, CellBrowser) implement keyboard navigation themselves: one element in the whole widget carries tabindex="0" (the keyboard-selected row or cell), and the arrow keys move that highlight and the real focus. Two enums configure it, on every AbstractHasData display (the list, table and grid) through HasKeyboardSelectionPolicy and HasKeyboardPagingPolicy:

Table 5. Keyboard policies of the cell widgets
Setting Values Meaning

setKeyboardSelectionPolicy

DISABLED, ENABLED (default), BOUND_TO_SELECTION

DISABLED turns keyboard navigation off (no cell gets a tabindex). ENABLED moves a focus highlight with the arrows. BOUND_TO_SELECTION also selects the row that becomes keyboard-selected in the SelectionModel, but only after the user has interacted with the widget once, so nothing is selected by default.

setKeyboardPagingPolicy

CURRENT_PAGE, CHANGE_PAGE (default), INCREASE_RANGE

CURRENT_PAGE: the keyboard cannot leave the visible page. CHANGE_PAGE: going past the end moves to the next page. INCREASE_RANGE: the range grows (Page Up/Down jump 30 rows).

setTabIndex, setAccessKey, setFocus

int, char, boolean

The Focusable methods of the widget: where the widget sits in the tab order, an access key, and moving focus into it.

Which keys do what

The keys are handled by AbstractHasData.DefaultKeyboardSelectionHandler, a CellPreviewEvent.Handler installed by default; the table widgets use the subclass AbstractCellTable.CellTableKeyboardSelectionHandler, which adds the horizontal movement between cells.

Table 6. Keys handled by the default handlers (keydown)
Key Effect

Down, Up

Next and previous row.

Page Down, Page Up

Next and previous page (CHANGE_PAGE), or 30 rows further (INCREASE_RANGE); nothing under CURRENT_PAGE.

Home, End

First and last row of the data.

Right, Left (tables only)

Next and previous interactive column; past the last one it wraps to the next row, past the first to the previous row. The two keys swap in a right-to-left locale (LocaleInfo.getCurrentLocale().isRTL()).

Space

keydown only prevents scrolling. With a SelectionModel and the default DefaultSelectionEventManager, the space bar toggles the selection on keyup.

Enter

Delivered to the cell: AbstractCell.onBrowserEvent calls onEnterKeyDown(...) for a keydown Enter, which ButtonCell, ClickableTextCell and ActionCell override to act like a click.

"Interactive" means a column whose cell consumes events: a TextColumn does not, a ButtonCell column does. In a table with no interactive column the horizontal keys have nothing to move between, so Right and Left just step through rows. The tabindex follows the keyboard-selected cell.

Measured in a CellTable

The lab table had two TextColumn`s (Title, Author) and a `ButtonCell column ("Borrow") over three Bookshelf rows. Each key was dispatched on the focused element; focus follows the keyboard selection a moment later, so each step reads the result in the next turn:

CellTable keyboard navigation (3 rows, column 2 is the only interactive one)
defaults       : policy=ENABLED paging=CHANGE_PAGE getTabIndex()=0
Down           : row=1 col=2 active=<div tabindex="0">            (the first interactive column gets the keyboard selection)
Right          : row=2 col=2                                       (no further interactive column: wrapped to the next row)
Right          : row=2 col=2                                       (end of the table: nothing moves)
End            : row=2 col=2
Home           : row=0 col=2
Down x3 more   : row=2 page=0                                      (stops at the last of the 3 rows, same page)
DISABLED       : the cell div has no tabindex any more

The same table showed these attributes: the SimplePager buttons are img elements with role="button", aria-label ("First page" and so on) and aria-disabled (written by the pager, Roles.getButtonRole().setAriaLabelProperty and setAriaDisabledState in SimplePager), while the table’s own table, header th and rows carry no ARIA attributes at all. CellTree is the cell widget that does: its root has role tree and every node role treeitem with aria-level, aria-setsize, aria-posinset and aria-expanded.

What you must add yourself

  • A name for the table. Give the widget’s element an aria-label or aria-labelledby, as in the review form above (CellTable extends Composite, so table.getElement() is the table element; Roles.getGridRole() is available if you want the grid semantics).

  • Keyboard access to your own cells. A cell must declare the events it uses in its constructor (super("click", "keydown")) and handle Enter in onEnterKeyDown. A span with a click handler is not focusable; use a button element, or tabindex plus Enter handling as ClickableTextCell does. Cell Widgets has the full cell-writing guide.

  • Not trapping the user. Keyboard navigation inside a cell widget uses Tab only to enter and leave. If you set KeyboardSelectionPolicy.DISABLED to avoid a focus highlight, you remove the way into the widget for keyboard users unless your cells are focusable elements.

  • Selection state. The selected style is only a CSS class on a CellTable row. Tell assistive technology about it yourself (aria-selected on the row element in a custom RowStyles or cell template) if the selection matters.

What the built-in widgets already expose

Before adding anything, see what the widget does. This table combines the GWT sources with the attributes read in the lab’s DOM.

Table 7. ARIA written by built-in widgets (GWT 2.13.1)
Widget Roles, states and properties

CustomButton, PushButton, ToggleButton

role="button" and aria-pressed (removed while the button is disabled). Lab: a ToggleButton showed role="button" aria-pressed="false" tabindex="0" and aria-pressed="true" after setDown(true).

Tree, TreeItem

Focusable element role="tree" with aria-activedescendant; item content role="treeitem" with aria-level, aria-setsize, aria-posinset, aria-expanded and aria-selected, written on selection (see above). Surrounding table elements are role="presentation".

MenuBar, MenuItem

Root role="menubar" with tabindex="0" (sub-menus -1) and aria-activedescendant; items role="menuitem", aria-haspopup on items that have a sub-menu. Lab: <div role="menubar" tabindex="0"> and <td role="menuitem">.

TabBar, TabPanel

role="tablist" on the bar, role="tab" on each tab, role="tabpanel" on the deck. Lab: <table role="tablist"> and <div role="tabpanel">.

CellTree

Root role="tree", nodes role="treeitem" with level, set size, position and expanded state.

SimplePager

Buttons with role="button", aria-label and aria-disabled.

DatePicker (DefaultCalendarView)

Day cells get aria-selected through the grid cell role.

StackPanel, DecoratorPanel, DisclosurePanel, CellPanel family

role="presentation" on the layout tables, so a screen reader does not announce a table.

UIObject.setVisible(boolean)

Sets aria-hidden="true" together with display: none when hiding and removes the attribute when showing. Lab: <div aria-hidden="true" style="display: none;">, then <div>.

Button, TextBox, ListBox, CheckBox, RadioButton

Nothing; they are native HTML elements and need nothing. Give them a name with a label element (LabelElement.setHtmlFor) or the ARIA properties above.

The guide’s statement that "all widgets that inherit from FocusWidget now have a tabindex by default" matches the code (FocusWidget.onAttach sets a tab index of 0 when it is -1), so a custom FocusWidget appears in the tab order without more work.

The deprecated Accessibility class

com.google.gwt.user.client.ui.Accessibility is the pre-2.5 API. It is a class of string constants (ROLE_TREE, ROLE_BUTTON, ROLE_TAB, …​, STATE_EXPANDED, STATE_PRESSED, …​) and four static methods, setRole, getRole, setState and removeState. It writes the attribute you name with the value you pass, so a typo is accepted silently. Its Javadoc marks it @Deprecated and tells you to use Roles instead:

Compiling code that still uses Accessibility
Dep.java:3: warning: [deprecation] Accessibility in com.google.gwt.user.client.ui has been deprecated
public class Dep { void f(Element e) { Accessibility.setRole(e, Accessibility.ROLE_BUTTON); } }

It still works in 2.13.1 and compiles with that warning (the old and new API wrote the same attributes in the lab: <div role="tree" aria-expanded="true">, and Roles.getTreeRole().set(...) then replaced the role). The mapping to the new API is mechanical:

Table 8. Migrating from Accessibility to Roles
Old (deprecated) New

Accessibility.setRole(el, Accessibility.ROLE_BUTTON)

Roles.getButtonRole().set(el)

Accessibility.getRole(el)

Roles.roleOf(el) returns the Role (its getName() is the string), or null if the role is not a known concrete one

Accessibility.setState(el, Accessibility.STATE_PRESSED, "true")

Roles.getButtonRole().setAriaPressedState(el, PressedValue.of(true))

Accessibility.setState(el, Accessibility.STATE_EXPANDED, "true")

Roles.getTreeitemRole().setAriaExpandedState(el, ExpandedValue.of(true))

Accessibility.setState(el, Accessibility.STATE_ACTIVEDESCENDANT, id)

Roles.getTreeRole().setAriaActivedescendantProperty(el, Id.of(id))

Accessibility.setState(el, Accessibility.STATE_LEVEL, "2")

Roles.getTreeitemRole().setAriaLevelProperty(el, 2)

Accessibility.removeState(el, name)

the role’s removeAriaXxxState or removeAriaXxxProperty method; Role.remove(el) removes the role itself

The old API has only the eight roles and eight states of its constants; the new one covers the full 1.0 vocabulary with compile-time checking.

Bookshelf worked example: an accessible book card

The Bookshelf catalogue page, shown with Cell Widgets, is a DataGrid/CellTable of books. Putting this page together for one book detail panel gives the following inventory, which is what the lab above built and ran:

Table 9. Accessibility inventory of a Bookshelf book panel
Element ARIA / focus Why

"Add to my loans" icon (PressableIcon)

role="button", aria-label, aria-pressed, tabindex="0", Enter and Space

An icon has no text; a toggle must say whether it is on.

Rating (RatingSlider)

role="slider", aria-valuemin/max/now/text, arrows, Home, End

A star row is a range, with a spoken word as well as a number.

Review form (ReviewForm)

aria-labelledby, aria-describedby, aria-required, aria-invalid, status live region

Name, hint and validation feedback without relying on colour or position.

Actions toolbar (RovingToolbar)

role="toolbar" with a name, roving tabindex

One Tab stop for the group, arrows inside.

Loan dialog (DialogBox)

role="dialog", aria-label, aria-modal as a plain attribute, focus in and back

DialogBox sets none of this.

Catalogue (CellTable)

Default keyboard policy, an aria-label for the table, a ButtonCell column for the action

Cell widgets bring their keyboard model; the name and the action are yours.

Testing accessibility

The guide gives three layers of checks, and they still apply:

  • The DOM. ARIA attributes are plain DOM attributes, so the browser developer tools show whether they are in the right place. This is the quickest check and the one used for every dump on this page.

  • The accessibility tree. Current browsers expose the computed tree (roles, names, states) in their developer tools; this is what a screen reader receives, and it is the modern replacement for the guide’s Windows-only event watcher.

  • A screen reader. The only real test of whether the result makes sense. The guide’s list of screen readers (JAWS, Window-Eyes, FireVox, ChromeVox) is dated: Window-Eyes, FireVox and the ChromeVox extension of that time are discontinued. Today test with NVDA, JAWS, VoiceOver and TalkBack.

Automated checkers and the WCAG tool list are in Web Accessibility; they cannot judge whether aria-valuetext is meaningful, which is why a manual pass remains necessary.

A GWT-specific caveat: assertions are on in Super Dev Mode, so assert failures inside the ARIA library (for example a null element) surface as errors during development and disappear from a production build (see Compiler & Compilation).

Common pitfalls

All messages below were produced by compiling or running the code in the lab, or are found in the 2.13.1 sources.

Table 10. Pitfalls and their exact symptoms
Mistake Symptom Fix

Copying the guide’s Roles.getTreeRole().setAriaActivedescendantProperty(focusable, IdReference.of(...))

error: cannot find symbol symbol: variable IdReference

The type is Id: Id.of(...). The snippet is from before the library was cleaned up for 2.5.0.

Copying the guide’s Roles.getRegionRole(ariaElement); to set a role

error: method getRegionRole in class Roles cannot be applied to given types; required: no arguments found: Element

The getter takes no argument; apply it with Roles.getRegionRole().set(element).

Setting a state the role does not support, such as Roles.getButtonRole().setAriaCheckedState(el, CheckedValue.TRUE)

error: cannot find symbol symbol: method setAriaCheckedState(Element,CheckedValue) location: interface ButtonRole

Choose the role that supports the state (getCheckboxRole(), getMenuitemcheckboxRole()), or write the attribute with setAttribute if you know better than the library.

Passing a boolean to an enum-typed state

error: incompatible types: boolean cannot be converted to PressedValue (and the same for InvalidValue)

Use PressedValue.of(boolean); for aria-invalid use InvalidValue.TRUE or FALSE, there is no of.

The Roles Javadoc example setAriaEnabledState(...)

It does not exist in the library.

ARIA has aria-disabled: setAriaDisabledState(element, !enabled).

The guide’s CustomButton snippet setInactive() and setActive()

The two methods set aria-disabled to the opposite values of what their names say.

Set aria-disabled to true when the widget is inactive, false when it is active, and check your own code the same way.

Two roles on one element

Roles.getButtonRole().set(x); Roles.getLinkRole().set(x); leaves <div role="link">; the first role is lost.

One role per element. Role.remove(element) removes it.

Id.of("") for an element that has no id yet

It is accepted (aria-labelledby="", which labels nothing). The library’s own assert is elementId != null || elementId.equals(""), so with assertions on, Id.of((String) null) fails with java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "elementId" is null instead of the intended message.

Give the target an id first (Document.get().createUniqueId()) and never rely on the assertion.

Calling FocusPanel.setFocus(true) and reading focus at once

active=BUTTON right after, the FocusPanel only on the next turn (Chrome, Safari).

Defer the read, or use a FocusWidget where possible.

Looking for Document.getActiveElement()

[ERROR] The method getActiveElement() is undefined for the type Document

JSNI: /*-{ return $doc.activeElement; }-*/ (see JSNI & Overlay Types).

Hiding with a style instead of setVisible(false)

aria-hidden is not written; the element may still be announced if only its opacity or position is changed.

setVisible(boolean) writes aria-hidden and display: none together; for visually hidden but spoken text use a dedicated CSS class instead.

KeyboardSelectionPolicy.DISABLED to remove the focus highlight

No tabindex on any cell: keyboard users cannot enter the widget.

Restyle the focus classes (cellTableKeyboardSelectedRow) instead.

Reading aria-level from a Tree item that was never selected

The attribute is absent on unselected items.

Do not rely on those attributes in tests; they are written on selection.

Recent changes and outdated material

  • The ARIA library is generated code built for ARIA 1.0 (every file carries the "auto-generated code" header), and nothing in the 2.13.1 sources extends it. If you need ARIA 1.1 or 1.2 attributes, set them as plain attributes.

  • The Developer Guide page still shows the pre-2.5 API names in two code samples (IdReference, Roles.getRegionRole(element)) and names obsolete screen readers; the library source and the compiler are the authority.

  • The GWT in Action book (2007) has no accessibility chapter and predates ARIA.

References