An input's error message needs aria-describedby to be read
Red text under an input is not attached to it. A screen reader reads the field, says nothing about why the value was rejected, and the user moves on none the wiser.
<input id="email" aria-invalid={!!error} aria-describedby={error ? "email-error" : undefined} />
{error && <p id="email-error">{error}</p>}aria-invalid says the value is wrong, aria-describedby makes the message part of the field's description, so both are read out when focus lands there. Pass undefined while there is no error rather than an id that nothing in the DOM has.
accessibilityfrontend