When selecting “Other” reveals a text box, or pressing “Add another address” inserts a new group of fields, sighted users see the change instantly — but keyboard and screen-reader users are left where they were, with no signal that anything appeared, and often discover the new fields only when an error sends them back.

The opposite mistake is just as common: moving focus every time anything appears, so choosing a radio option yanks focus into a follow-up field the user was not ready for. This page, part of focus management after validation, sets out when focus should move, when it should stay and be announced instead, and how to time the move so the new element exists when you focus it.


Context and prerequisites

Three kinds of dynamic field, with different rules:

  • Revealed follow-up questions — choosing “Other” shows “Please specify”; ticking “Different billing address” shows an address block. The user is still interacting with the triggering control (arrowing through radios, perhaps). Do not move focus. Place the new content immediately after the trigger in DOM order so the next Tab reaches it, and make its appearance perceivable.
  • User-requested additions — pressing “Add another item” or “Add a contact”. The user asked for new fields and wants to fill them. Move focus to the first new field.
  • Error-driven reveals — a submit fails because a field inside a collapsed section is invalid. Reveal, then focus, via the error summary or first-invalid-field logic.

In every case, the new content must come after the trigger in the DOM, so sequential navigation finds it without searching.

Should focus move? When choosing Other reveals a text field, focus should not move; the field is placed right after the options and the reveal is indicated by aria-expanded or a hint. When a checkbox reveals a billing address block, focus should not move; the block follows the checkbox in DOM order. When the user presses Add another item, focus should move to the new row's first field and the addition should be announced. When a failed submit reveals a collapsed section, the section is expanded and focus moves to the invalid field through the error summary. Situation Move focus? Where / what else "Other" reveals a text box no next in DOM order; hint or aria-controls checkbox reveals address block no block directly after the checkbox "Add another item" yes new row's first field; announce submit fails inside collapsed section yes expand, then focus the field

The core pattern: focus after the DOM exists, only when requested

import { useEffect, useRef, useState } from "react";

// 1) User-requested addition: focus the new row's first field AFTER it renders.
export function useFocusNewRow<T extends { id: string }>(rows: T[]) {
  const pendingFocus = useRef<string | null>(null);
  const firstFieldRefs = useRef(new Map<string, HTMLInputElement>());

  // Runs after commit, so the new input exists in the DOM.
  useEffect(() => {
    const id = pendingFocus.current;
    if (!id) return;
    pendingFocus.current = null;
    firstFieldRefs.current.get(id)?.focus();
  }, [rows]);

  return {
    requestFocus: (id: string) => { pendingFocus.current = id; },
    registerFirstField: (id: string) => (el: HTMLInputElement | null) => {
      if (el) firstFieldRefs.current.set(id, el); else firstFieldRefs.current.delete(id);
    },
  };
}

// 2) Revealed follow-up: no focus move; expose the relationship.
export function OtherReason() {
  const [reason, setReason] = useState("");
  return (
    <fieldset>
      <legend>Why are you cancelling?</legend>
      {["Too expensive", "Missing features", "Other"].map((r) => (
        <div key={r}>
          <input type="radio" id={`r-${r}`} name="reason" value={r}
                 checked={reason === r} onChange={() => setReason(r)}
                 aria-controls={r === "Other" ? "other-detail" : undefined}
                 aria-expanded={r === "Other" ? reason === "Other" : undefined} />
          <label htmlFor={`r-${r}`}>{r}</label>
        </div>
      ))}
      {/* Immediately after the options in DOM order: the next Tab reaches it. */}
      {reason === "Other" && (
        <div id="other-detail">
          <label htmlFor="other-text">Tell us more (optional)</label>
          <input id="other-text" name="otherText" />
        </div>
      )}
    </fieldset>
  );
}

In Vue, move focus in nextTick() after the state change (or in an onUpdated guarded by a pending flag); in Svelte, await tick() before calling focus(). The rule is the same everywhere: change state, wait for the framework to render, then focus.


Step-by-step walkthrough

  1. Classify the trigger. Did the user ask for new fields (move focus), or did an answer reveal a follow-up (do not move)?
  2. Place new content right after its trigger in DOM order. Visual placement elsewhere (a side panel) breaks sequential navigation; keep DOM order and reading order aligned.
  3. Focus after render. Record the intent in the event handler, and perform the focus in an effect, nextTick or tick — focusing an element that does not exist yet silently fails.
  4. Announce additions. “Item 3 added” through the page announcer, so the change is confirmed even though focus moved — see throttling live region announcements.
  5. Expose reveal relationships. aria-controls and aria-expanded on disclosure-like triggers (checkboxes that show sections) tell assistive technology that more content is available.
  6. Label new groups with their position. “Item 3 of 3” in a legend lets users confirm where they are after focus moves, as in dynamic field arrays and repeatable groups.

Why revealed questions should not steal focus

A radio group is operated with arrow keys, and arrowing selects. A keyboard user comparing options by arrowing past “Other” would, with an auto-focus rule, be thrown into the “Please specify” box every time they pass it — and arrowing further would then edit text instead of changing the selection. Even with a mouse, pulling focus away as soon as something is clicked prevents users from changing their minds. Leaving focus on the trigger and placing the follow-up next in order respects the user’s pace: they reach the new field with one Tab when they are ready.

Adding a row, with focus and announcement The user presses Add another item. The click handler adds a row with a new id to state and records that the new row's first field should receive focus. The framework renders the new row. After the commit, the effect finds the pending id and focuses the new row's description field. The announcer says item 3 added after a short pause, and the screen reader reads the new field's label and the legend item 3 of 3. User Handler Framework Announcer Add another item rows += new; pendingFocus = id committed: effect runs focus → new row's first field "Item 3 added"

Failure modes and edge cases

1. Focusing too early

Calling focus() in the click handler, before the re-render, targets nothing (or the previous row). Always focus after the framework has committed the DOM.

2. Content inserted before the trigger

Rows added at the top of a list, or follow-up questions rendered above their trigger, are skipped by Tab and read out of order. Insert after the trigger, or move focus explicitly when order cannot be changed.

3. Animation delays

If new content animates in (height from 0), focusing during the animation can scroll oddly or fail on display: none stages. Focus when the element is focusable (not hidden), and respect prefers-reduced-motion to skip the animation.

4. Hidden then invalid

A revealed field that becomes invalid and then is hidden again by another answer must not keep blocking submit. Apply relevance rules, per what happens to errors when a field is hidden.

5. Scroll position on mobile

Focusing a newly added field scrolls it into view, but on mobile the on-screen keyboard can cover it. Ensure scroll-margin accounts for sticky headers and footers, as in scrolling invalid fields into view under sticky headers.

Placement rules for new content A revealed follow-up belongs immediately after its trigger control, so the next Tab reaches it. A new repeatable row belongs at the end of the list, just before the Add button, so focus moves forward. A revealed section in response to an error belongs where it already was, expanded in place, so the error summary link lands in the right context. Follow-up question Right after the trigger. Focus stays; next Tab reaches it. New row End of the list, before Add. Focus moves into it. Error reveal Expanded in place. Summary link focuses the field.

Verification checklist


Frequently Asked Questions

Should the "Please specify" field be required when revealed?

Only if you genuinely need the detail. If it is required, say so in its label and validate it only when relevant; an optional “Tell us more” is kinder and avoids trapping users who picked “Other” for lack of a better option.

Is autofocus on the new field acceptable?

The autofocus attribute only applies on page load or dialog open; it does not fire for elements inserted later. Focus programmatically after render instead.

What about content that appears after an async load?

If the user triggered the load (pressing “Find address”), move focus to the results or announce them when they arrive. If it loads by itself, do not move focus; announce politely if it matters.


Related

← Focus Management After Validation