O problema
A validação padrão do HTML5 fornece feedback visual (via :invalid ou :user-invalid), mas não sincroniza automaticamente com atributos de acessibilidade, como aria-invalid.
Se você usar o estilo padrão de :invalid, os leitores de tela poderão anunciar "Entrada inválida" no momento em que um usuário acessar um campo obrigatório que esteja vazio. Isso cria uma experiência ruim para os usuários que usam tecnologias adaptativas, já que o erro é anunciado antes que a interação ocorra.
A solução
Queremos que o estado programático (aria-invalid="true") seja aplicado no exato momento em que o estado visual (:user-invalid) é aplicado. Como :user-invalid depende da flag interna "user-interacted" do navegador, podemos usar o JavaScript para verificar se esse seletor corresponde durante eventos de interação padrão.
Consulte MDN aria-invalid para mais detalhes.
Estratégia de implementação
- Camada visual: use o CSS
:user-invalidpara mostrar bordas/ícones. - Camada de acessibilidade: use
aria-invalidearia-errormessagepara comunicar o estado à tecnologia adaptativa (TA). - Camada visual e de acessibilidade de ponte: crie um utilitário JavaScript leve que detecte eventos
blureinput, verifique se o elemento corresponde a:user-invalide atualize os atributos ARIA de acordo.
Guia de implementação
1. Estrutura HTML
Vincule a entrada à mensagem de erro usando aria-errormessage (ou aria-describedby para um suporte mais amplo).
<form>
<div class="field">
<label for="email">Email</label>
<input
type="email"
id="email"
required
aria-errormessage="email-error"
>
<span id="email-error" class="error-msg">
Please enter a valid email address.
</span>
</div>
</form>
2. CSS
Controle a visibilidade da mensagem de erro usando a pseudoclasse nativa :user-invalid.
.error-msg {
display: none;
color: #d93025;
}
/* Show error message when input is user-invalid */
input:user-invalid ~ .error-msg {
display: block;
}
/* Optional: Visual cues on the input itself */
input:user-invalid {
border-color: #d93025;
}
3. JavaScript
Como não há um evento "UserInvalidChanged", use eventos de formulário padrão para verificar o estado.
const updateAriaState = (event) => {
const input = event.target;
if (!input.matches?.('input, textarea, select')) return;
// Check if the browser currently considers this input "user-invalid"
const isUserInvalid = input.matches(':user-invalid');
if (isUserInvalid) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
// Listen on the document to handle dynamically added fields.
// 'blur' and 'focus' do not bubble, so we must use the capture phase (true).
document.addEventListener('blur', updateAriaState, true);
document.addEventListener('focus', updateAriaState, true);
// Also update on input if we've already shown the error,
// so the error clears immediately when fixed.
document.addEventListener('input', (event) => {
const input = event.target;
if (!input.matches?.('input, textarea, select')) return;
const hasAriaInvalid = input.hasAttribute('aria-invalid');
const ariaInvalid = input.getAttribute('aria-invalid');
if (hasAriaInvalid && ariaInvalid === 'true') {
updateAriaState(event);
}
});
Fallback e suporte ao navegador
Detecção de recursos
É possível verificar a compatibilidade em CSS e JavaScript.
Verificação de JavaScript:
if (!CSS.supports('selector(:user-invalid)')) {
// Fallback logic here
}
CSS para substituição
Para garantir que a lógica de substituição seja visualmente indistinguível do comportamento nativo, aplique os estilos de erro à pseudoclasse e à classe de substituição.
/* Apply error styles to both native selector and fallback class */
input:user-invalid,
input.user-invalid-fallback {
border-color: #d93025;
background-color: #fce8e6;
}
/* Show error message for both cases */
input:user-invalid ~ .error-msg,
input.user-invalid-fallback ~ .error-msg {
display: block;
}
Lógica de substituição
Se :user-invalid estiver ausente, rastreie manualmente o estado de interação usando um WeakMap.
const UserInvalidFallback = (() => {
const dirtyState = new WeakMap();
const updateState = (input) => {
const isValid = input.checkValidity();
// Update both visual and ARIA state
input.classList.toggle('user-invalid-fallback', !isValid);
input.classList.toggle('user-valid-fallback', isValid);
if (!isValid) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
const handleEvent = (event) => {
const input = event.target;
if (event.type === 'reset' && input.matches?.('form')) {
const controls = input.elements || [];
for (const control of controls) {
dirtyState.delete(control);
control.classList.remove('user-invalid-fallback');
control.classList.remove('user-valid-fallback');
control.removeAttribute('aria-invalid');
}
return;
}
if (!input.matches?.('input, textarea, select')) return;
if (event.type === 'input' || event.type === 'change') {
const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
state.hasInteracted = true;
dirtyState.set(input, state);
if (state.hasBlurred) {
updateState(input);
}
} else if (event.type === 'blur') {
const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
state.hasBlurred = true;
dirtyState.set(input, state);
if (state.hasInteracted) {
updateState(input);
}
}
};
const init = () => {
if (CSS.supports('selector(:user-invalid)')) return;
document.addEventListener('blur', handleEvent, true); // Capture phase required
document.addEventListener('input', handleEvent, true);
document.addEventListener('change', handleEvent, true);
document.addEventListener('reset', handleEvent, true); // Capture resets
};
return { init };
})();
// Initialize globally
UserInvalidFallback.init();
Outras considerações
aria-livexaria-errormessage:aria-errormessageconecta a entrada ao texto, mas os leitores de tela podem não anunciá-la imediatamente após o aparecimento (somente ao focar na entrada).- Se você precisar de um aviso imediato quando o erro aparecer (por exemplo, ao desfocar), adicione
role="alert"ouaria-live="polite"ao contêiner da mensagem de erro, mas teste bem para evitar um "aviso duplo" quando o usuário focar no campo para corrigir o problema.
Internacionalização:
- Verifique se o conteúdo de texto da mensagem de erro (
#email-error) está traduzido. A lógica permanece a mesma.
- Verifique se o conteúdo de texto da mensagem de erro (