Lesson 21 · Part 4 · Relationships

Relationships and grid indices

Relationship attributes connect one element to another by id: "this input is described by that hint". They do not move DOM nodes. The exception is aria-owns, which changes the accessibility tree's parent. Get an id wrong and the relationship is silently missing, with no error.

In one minute

In plain words

Imagine sticky notes with names on them. A relationship attribute says "read the note called hint when you get to this box". If the note is called hnt, nobody says anything. The page still looks perfect. Only the person who relies on the note is left without it.

1. Active descendant: focus stays, the pointer moves

In a listbox, real focus can stay on the container while aria-activedescendant holds the id of the option that is "active". Arrow keys change the attribute. The options are not tab stops. This is how a combobox keeps focus in the text field while the user moves through suggestions.

Step 1. Click the list or Tab to it, then press ↓ and ↑. Compare both versions.

Lisbon
Oslo
Seoul

          
Move with the arrows.

The target must be a DOM descendant of the focused element (or be listed in its aria-owns). Clear the attribute when the popup closes. Keep the highlighted item visible on screen too: scroll it into view as the user moves (WCAG 2.4.7 Focus Visible).

2. Describing and error messages: ids must match

aria-describedby points to a hint. aria-errormessage points to the failure text, and is only used when aria-invalid="true". The error text should be visible. Use the lab to see what a typo does.

Step 2. Pick a version, then press "Validate" with the field empty.

Use at least 8 characters.

    
              
    Press Validate.

    Always give the error a text marker (here "✕") and not just red. Related: aria-details points to longer content and is covered in lesson 16. Ids must be unique on the page. Duplicate ids mean the pointer lands on the first match, usually the wrong one.

    3. Position in a set that is not all there

    A long list is often virtualized: only a few rows exist in the page and the rest are created as you scroll. Without help, the screen reader says "1 of 5" for the first visible row, which is wrong. aria-setsize gives the true total. aria-posinset gives this item's real place, starting at 1.

    Step 3. Drag the scroll position. Only 5 rows are in the DOM. Toggle the attributes and read the first row.

      Move the slider.

      If every item of the set is in the DOM, leave both off. The browser counts the children correctly by itself.

      4. Grid indices: row 41 of 1000

      The same idea for a grid. aria-rowcount and aria-colcount go on the grid. aria-rowindex goes on each row and aria-colindex on each cell. All start at 1. Use -1 for a total you do not know.

      Step 4. This grid is 1000 rows by 12 columns, but only 3 by 3 cells are in the page. Click a cell.

      
                
      Click a cell.
      Click a cell.

      A plain HTML table with every cell in the DOM needs none of this. Also available: aria-rowindextext and aria-colindextext for human labels such as "Q3", and aria-rowspan and aria-colspan matching HTML rowspan and colspan (set on the spanning cell only).

      5. Quick check: which attribute?

      Choose an answer.

      6. Reference: every relationship attribute

      aria-activedescendantId of the focused descendant while DOM focus remains on the composite. The target must be a DOM descendant, or an aria-owns descendant. Clear it when the popup closes.
      aria-controlsIds of elements this control affects, such as a tab’s panel or a disclosure’s region. It does not show or hide them. Your script does. It tells the user the relationship exists.
      aria-ownsIds of elements that should be treated as children in the accessibility tree even though they are not DOM children. Use it when a portal renders the popup at the end of body. Do not use it on a role whose children are presentational. Do not use it to fake a parent you could have written in HTML.
      aria-flowtoIds of the next recommended reading targets, an alternative to source order. Rare. If you need it, the visual order and the DOM order have already diverged too far. Fix the order first.
      aria-errormessageId of the error for an aria-invalid field. The message element should be visible. Describedby can also point at the error. Errormessage is the specific relationship for the failure text.
      aria-detailsCovered in lesson 16. It is a relationship to longer content, not a name and not a live region.
      aria-posinsetThis item’s position in a set, starting at 1, when the full set is not in the DOM. A virtualized tree uses it so “3 of 200” is still true while only 20 rows are mounted.
      aria-setsizeThe size of that set. Set it together with posinset. If the whole set is in the DOM, omit both and let the tree count the children.
      aria-levelAlso a relationship to structure: heading level or tree depth when nesting is flattened for virtualization.
      aria-owns is a last resort

      It rewrites the accessibility tree's parent and child links, which can reorder what users hear. If you can write the child inside its parent in HTML, do that. aria-flowto is a sign that visual and DOM order have drifted apart. Fix the order first.

      7. Reference: grid and table indices

      Use these when the grid is virtualized or spanned, so the accessibility tree can still report "row 40 of 1000, column 3".

      aria-rowcountTotal rows, including rows not in the DOM. Use −1 if the total is unknown.
      aria-colcountTotal columns, the same way.
      aria-rowindexThis row’s index, starting at 1, in the full table.
      aria-colindexThis cell’s column index, starting at 1.
      aria-rowindextextA human string instead of the row number, when the number is not what should be spoken.
      aria-colindextextA human string instead of the column number.
      aria-rowspanHow many rows this cell spans. Matches the HTML rowspan attribute. Set it on the cell that spans, not on the cells it covers.
      aria-colspanHow many columns this cell spans. Same idea as HTML colspan.

      8. Drag and drop attributes are deprecated

      Do not add these to new work. Give every drag a keyboard path with buttons such as "Move up" and "Move down" (WCAG 2.5.7 Dragging Movements).

      aria-grabbedWas true, false, or undefined for a draggable object. Deprecated. Do not add it to new work. A drag operation still needs a keyboard path: buttons that pick up and drop, not only a pointer gesture.
      aria-dropeffectWas a token list of what a drop would do: copy, move, link, execute, popup, none. Deprecated. The drop target should be a real control with a name that says what will happen.

      Common mistakes

      You seeWhy it is a problemFix
      Typo in an id referenceSilently ignored.Check getElementById for every reference.
      Duplicate idsPointer hits the wrong element.Make ids unique.
      Arrow keys highlight but aria-activedescendant never changesScreen reader keeps reading the first item.Update the attribute on every move.
      Virtualized list with no aria-setsize"1 of 5" instead of "43 of 200".Set size and position.
      aria-owns to fake a parentScrambles reading order.Write the real HTML parent.
      New work with aria-grabbedDeprecated.Buttons with names.

      What you have now

      Lessons 15 through 21 cover the WAI-ARIA 1.3 surface used in real pages: the model, every naming attribute, every widget state, standalone and composite roles, structure and landmarks, live regions, window roles, ariaNotify, relationships, and grid indices. Abstract roles stay off the page. Native HTML stays first. When a state changes, the attribute changes with it.

      Try it yourself
      1. In lab 1, switch to "Attribute updated". In the browser's accessibility panel, watch aria-activedescendant change as you press the arrows.
      2. In lab 2, rename one id in dev tools and see the reference check turn red.

      Remember

      Edit this example in the playground