Acessibilidade em Formulários
Labels, erros anunciados para VoiceOver/TalkBack e gerenciamento de foco.
Busque em todas as páginas da documentação
Labels, erros anunciados para VoiceOver/TalkBack e gerenciamento de foco.
Cartão de receita de referência rápida - pronto para copiar e colar.
import { useRef } from "react";
import {
AccessibilityInfo,
Pressable,
Text,
TextInput,
View,
} from "react-native";
export function AccessibleField({
label,
hint,
error,
value,
onChangeText,
}: {
label: string;
hint?: string;
error?: string;
value: string;
onChangeText: (t: string) => void;
}) {
const inputRef = useRef<TextInput>(null);
return (
<View style={{ gap: 4 }}>
<Text nativeID="emailLabel" style={{ fontWeight: "600" }}>
{label}
</Text>
<TextInput
ref={inputRef}
value={value}
onChangeText={onChangeText}
accessibilityLabel={label}
accessibilityHint={hint}
accessibilityLabelledBy="emailLabel"
accessibilityInvalid={!!error}
/>
{error ? (
<Text
accessibilityRole="alert"
accessibilityLiveRegion="assertive"
style={{ color: "#dc2626" }}
>
{error}
</Text>
) : null}
</View>
);
}
export async function announceFormError(message: string) {
await AccessibilityInfo.announceForAccessibility(message);
}
export function focusFirstError(
refs: Array<React.RefObject<TextInput | null>>,
errors: boolean[],
) {
const idx = errors.findIndex(Boolean);
if (idx >= 0) refs[idx]?.current?.focus();
}Quando usar isso: Qualquer formulário de produção - revisão de loja, aquisição empresarial e usuários reais com VoiceOver/TalkBack ativados. Formulários móveis falham em auditorias de acessibilidade com mais frequência por rótulos ausentes, erros silenciosos e perda de foco após a validação.
import { zodResolver } from "@hookform/resolvers/zod";
import { forwardRef, useRef } from "react";
import { Controller, useForm } from "react-hook-form";
import {
AccessibilityInfo,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import { z } from "zod";
const schema = z.object({
name: z.string().min(2, "O nome deve ter pelo menos 2 caracteres"),
email: z.string().email("Insira um endereço de e-mail válido"),
phone: z
.string()
.regex(/^\d{10}$/, "Insira um número de telefone de 10 dígitos sem espaços"),
});
type FormValues = z.infer<typeof schema>;
const FIELD_ORDER = ["name", "email", "phone"] as const;
export function ContactForm({ onSuccess }: { onSuccess: () => void }) {
const nameRef = useRef<TextInput>(null);
const emailRef = useRef<TextInput>(null);
const phoneRef = useRef<TextInput>(null);
const refs = [nameRef, emailRef, phoneRef];
const {
control,
handleSubmit,
formState: { errors },
setFocus,
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { name: "", email: "", phone: "" },
mode: "onTouched",
});
const onInvalid = async () => {
const first = FIELD_ORDER.find((k) => errors[k]);
if (first) {
setFocus(first);
const message = errors[first]?.message ?? "Corrija os campos destacados";
await AccessibilityInfo.announceForAccessibility(message);
}
};
const onSubmit = handleSubmit(async (data) => {
await fakeSubmit(data);
await AccessibilityInfo.announceForAccessibility("Mensagem enviada com sucesso");
onSuccess();
}, onInvalid);
return (
<ScrollView
contentContainerStyle={styles.container}
keyboardShouldPersistTaps="handled"
>
<Text style={styles.heading} accessibilityRole="header">
Fale conosco
</Text>
<Text style={styles.sub}>
Os campos obrigatórios estão marcados. Os erros são anunciados automaticamente.
</Text>
<Controller
control={control}
name="name"
render={({ field, fieldState }) => (
<Field
ref={nameRef}
label="Nome completo"
required
value={field.value}
onChangeText={field.onChange}
onBlur={field.onBlur}
error={fieldState.error?.message}
/>
)}
/>
<Controller
control={control}
name="email"
render={({ field, fieldState }) => (
<Field
ref={emailRef}
label="E-mail"
required
hint="Respondemos em um dia útil"
keyboardType="email-address"
autoCapitalize="none"
value={field.value}
onChangeText={field.onChange}
onBlur={field.onBlur}
error={fieldState.error?.message}
/>
)}
/>
<Controller
control={control}
name="phone"
render={({ field, fieldState }) => (
<Field
ref={phoneRef}
label="Telefone"
hint="Dez dígitos, sem traços"
keyboardType="phone-pad"
value={field.value}
onChangeText={field.onChange}
onBlur={field.onBlur}
error={fieldState.error?.message}
/>
)}
/>
<Pressable
style={styles.submit}
onPress={onSubmit}
accessibilityRole="button"
accessibilityLabel="Enviar mensagem"
>
<Text style={styles.submitText}>Enviar</Text>
</Pressable>
</ScrollView>
);
}
type FieldProps = {
label: string;
value: string;
onChangeText: (t: string) => void;
onBlur: () => void;
error?: string;
hint?: string;
required?: boolean;
keyboardType?: "default" | "email-address" | "phone-pad";
autoCapitalize?: "none" | "sentences";
};
const Field = forwardRef<TextInput, FieldProps>(function Field(
{
label,
value,
onChangeText,
onBlur,
error,
hint,
required,
keyboardType = "default",
autoCapitalize = "sentences",
},
ref,
) {
const labelId = `${label.replace(/\s/g, "")}Label`;
const errorId = `${label.replace(/\s/g, "")}Error`;
const a11yLabel = required ? `${label}, obrigatório` : label;
return (
<View style={styles.field}>
<Text nativeID={labelId} style={styles.label}>
{label}
{required ? " *" : ""}
</Text>
<TextInput
ref={ref}
style={[styles.input, error && styles.inputError]}
value={value}
onChangeText={onChangeText}
onBlur={onBlur}
keyboardType={keyboardType}
autoCapitalize={autoCapitalize}
accessibilityLabel={a11yLabel}
accessibilityHint={hint}
accessibilityLabelledBy={labelId}
accessibilityInvalid={!!error}
/>
{error ? (
<Text
nativeID={errorId}
accessibilityRole="alert"
accessibilityLiveRegion="assertive"
style={styles.error}
>
{error}
</Text>
) : null}
</View>
);
});
async function fakeSubmit(_data: FormValues) {
await new Promise((r) => setTimeout(r, 300));
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 16 },
heading: { fontSize: 24, fontWeight: "700" },
sub: { fontSize: 14, color: "#64748b", marginBottom: 4 },
field: { gap: 6 },
label: { fontSize: 14, fontWeight: "600" },
input: {
borderWidth: 1,
borderColor: "#d1d5db",
borderRadius: 8,
padding: 14,
fontSize: 16,
},
inputError: { borderColor: "#dc2626" },
error: { fontSize: 13, color: "#dc2626" },
submit: {
backgroundColor: "#2563eb",
padding: 16,
borderRadius: 8,
alignItems: "center",
marginTop: 8,
},
submitText: { color: "#fff", fontWeight: "600", fontSize: 16 },
});O que isso demonstra:
Text com nativeID associado a accessibilityLabelledBy no TextInput.accessibilityHint para orientações de formato; accessibilityLiveRegion="assertive" para texto de validação.accessibilityInvalid sinaliza o campo para tecnologia assistiva quando o Zod falha.onInvalid foca o primeiro campo inválido e chama announceForAccessibility.TextInput deve expor nome (rótulo), função (textbox é o padrão), valor, dica e estado (desabilitado, inválido).Text visível.accessibilityLiveRegion="polite" ou "assertive" pede ao TalkBack/VoiceOver para lê-lo. Para erros em lote no momento do envio, AccessibilityInfo.announceForAccessibility fornece um resumo falado único..focus() no primeiro TextInput inválido após o envio falho corresponde ao focus() da web em campos aria-invalid - crítico porque usuários de dispositivos móveis podem não ver o topo de um ScrollView longo.accessibilityRole (por exemplo, "radiogroup") e um rótulo de grupo, não três caixas de texto isoladas.| Prop | Propósito | Exemplo |
|---|---|---|
accessibilityLabel | Nome falado quando o rótulo visível é insuficiente ou apenas ícone | "E-mail, obrigatório" |
accessibilityHint | Como interagir / expectativa de formato | "Dez dígitos, sem traços" |
accessibilityLabelledBy | Ligar ao nativeID do rótulo visível (Android + iOS 13+) | emailLabel |
accessibilityInvalid | Marca o campo como falhando na validação | true quando há erro |
accessibilityState | Desabilitado, selecionado, marcado, expandido | { disabled: true } no envio |
accessibilityLiveRegion | Anunciar texto de erro dinâmico | "assertive" no Text de erro |
accessibilityRole | Substituição de função semântica | "button" no Pressable de envio personalizado |
// 1. Erro inline - montagem/desmontagem aciona a região ativa
{error && (
<Text accessibilityRole="alert" accessibilityLiveRegion="assertive">
{error}
</Text>
)}
// 2. Resumo do envio - uma mensagem falada para múltiplas falhas
const count = Object.keys(errors).length;
if (count > 0) {
await AccessibilityInfo.announceForAccessibility(
`${count} campos precisam de atenção. ${firstMessage}`,
);
}
// 3. Confirmação de sucesso - não confie apenas em toast
await AccessibilityInfo.announceForAccessibility("Perfil salvo");import type { TextInput } from "react-native";
import type { RefObject } from "react";
type InputRef = RefObject<TextInput | null>;
function focusField(ref: InputRef) {
ref.current?.focus();
}
// react-hook-form setFocus é tipado para nomes de campo
import type { FieldPath } from "react-hook-form";
function focusByName<T extends Record<string, unknown>>(
setFocus: (name: FieldPath<T>) => void,
name: FieldPath<T>,
) {
setFocus(name);
}forwardRef preservam a tipagem de ref para gerenciamento de foco.accessibilityLabelledBy aceita um nativeID de string nas versões recentes do iOS/Android; volte para accessibilityLabel sozinho em alvos mais antigos, se necessário.nativeIDs dos rótulos únicos por tela - colisões quebram a associação do rótulo.Rótulos apenas com placeholder - VoiceOver lê o campo vazio como "campo de texto" sem nome. Correção: Rótulo Text visível + accessibilityLabel.
Erros mostrados apenas em borda vermelha - Usuários daltônicos e usuários de leitores de tela perdem a falha. Correção: Erro de texto + accessibilityInvalid + região ativa ou anúncio.
Botões de limpar apenas com ícone - Anunciados como "botão" sem rótulo. Correção: accessibilityLabel="Limpar campo de e-mail".
Envio sem mover o foco - O usuário não ouve nada e assume que o aplicativo travou. Correção: onInvalid → setFocus(firstError) + announceForAccessibility.
Anúncios duplicados - Região ativa em cada campo dispara seis vezes no envio. Correção: Erros inline por campo ao perder o foco; anúncio único no envio.
Gatilhos de seletor personalizados sem valor - "Botão" sem a data atual falado. Correção: accessibilityLabel={Data, ${formatted}} - veja o artigo sobre seletores.
Envio desabilitado sem explicação - O estado disabled do botão pode não explicar o porquê. Correção: accessibilityState={{ disabled: true }} e texto auxiliar acima quando o formulário estiver incompleto.
keyboardShouldPersistTaps omitido - Toque duplo do VoiceOver no envio não dispara enquanto o teclado está aberto. Correção: "handled" no ScrollView do formulário / KeyboardAwareScrollView.
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
Rótulo visível + accessibilityLabelledBy | Padrão para todos os campos de texto | Pesquisa decorativa de campo único com accessibilityLabel explícito no input |
accessibilityLiveRegion no Text de erro | Erros inline de campo após perder o foco/envio | Erros de servidor em lote - use announceForAccessibility uma vez |
AccessibilityInfo.announceForAccessibility | Resumo do envio, toast de sucesso, mudanças de etapa | Substituir completamente os rótulos por campo |
Primitiva Field do design system | Forçar contrato de rótulo/erro/dica em todo o aplicativo | Protótipo único - ainda copie o padrão de props |
eslint-plugin-react-native-a11y | Proteção de CI para rótulos ausentes em TextInput | Substituição para testes em dispositivo com tecnologia assistiva real |
| Configurações de acessibilidade da plataforma | Respeitar AccessibilityInfo.isReduceMotionEnabled para validação animada | Construir um "modo acessível" separado - incorporar na UI padrão |
accessibilityLabel que o corresponda ou o complemente.accessibilityLabel="Pesquisar" explícito no TextInput.accessibilityLiveRegion ou accessibilityRole="alert" está definido.announceForAccessibility envia um anúncio global - útil após o envio quando o foco muda.hint - erros mudam; dicas devem ser instruções estáveis.accessibilityLabel ou no texto do rótulo visível.accessibilityState não tem required - o rótulo falado é a abordagem confiável.* sem a palavra "obrigatório".react-hook-form: handleSubmit(onValid, onInvalid) e setFocus(firstFieldName) em onInvalid.refs.find + .focus() no primeiro campo com erro.ScrollView - scrollTo ou refs de rolagem ciente do teclado.Switch precisa de seu próprio accessibilityLabel descrevendo o que ele alterna.accessibilityRole="radio" / radiogroup em um cluster Pressable.accessibilityState={{ checked: value }}.secureTextEntry - o rótulo ainda deve ser "Senha, obrigatório".hint - repita regras concisas em texto auxiliar visível.setError("email", { message }) do formulário.announceForAccessibility em um Text de banner com accessibilityRole="alert".announceForAccessibility("Etapa 2 de 4, Endereço de entrega").requestAnimationFrame).eslint-plugin-react-native-a11y captura accessibilityLabel ausente em alguns componentes.Pressable com accessibilityRole="button" e rótulo incluindo o valor atual.maxFontSizeMultiplier em telas densas apenas com cuidado.TextInput, keyboardType e cadeia de focosetFocus, setError e onInvalidaccessibilityRole e accessibilityState em PressableVersões do Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026