Form hiển thị đẹp khi chưa nhập, nhưng ô mật khẩu hoặc nút gửi biến mất lúc bàn phím mở. Thêm một khoảng trống cố định có thể sửa được trên máy của bạn rồi lại hỏng trên màn hình nhỏ, font lớn hoặc một navigator có header khác.
Bài viết tập trung vào bố cục và thao tác bàn phím, dành cho người đã biết TextInput và state cơ bản. API được đối chiếu tài liệu React Native ngày 22/10/2026. Hành vi cần được kiểm tra cùng phiên bản React Native, cấu hình cửa sổ Android và navigation của dự án; không có một giá trị offset cố định đúng cho mọi màn hình.
Chia trách nhiệm cho từng lớp
KeyboardAvoidingView điều chỉnh bố cục khi bàn phím xuất hiện. ScrollView cung cấp vùng cuộn khi nội dung lớn hơn phần còn lại. keyboardShouldPersistTaps ảnh hưởng cách một lần chạm được chuyển tới phần tử con khi bàn phím đang mở.
Ba trách nhiệm này liên quan nhưng không thay thế nhau. Một form có thể tránh được bàn phím nhưng không cuộn vì parent thiếu chiều cao hữu hạn. Hoặc nút đã nhìn thấy nhưng lần chạm đầu chỉ đóng bàn phím, khiến người dùng tưởng nút không phản hồi.
Bố cục khởi đầu cho form ngắn
Ví dụ nhận callback onSubmit từ ứng dụng; không triển khai xác thực và không ghi credential ra log. Màn hình cha chịu trách nhiệm xử lý safe area và truyền offset phù hợp với vị trí thực của vùng form.
import React, {useState} from 'react';
import {
KeyboardAvoidingView, Platform, Pressable,
ScrollView, StyleSheet, Text, TextInput,
} from 'react-native';
type Props = {
keyboardOffset?: number;
onSubmit: (value: {email: string; password: string}) => void;
};
export function LoginForm({keyboardOffset = 0, onSubmit}: Props) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<KeyboardAvoidingView
style={styles.screen}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={keyboardOffset}>
<ScrollView
style={styles.screen}
contentContainerStyle={styles.content}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag">
<Text>Email</Text>
<TextInput
accessibilityLabel="Email"
style={styles.input}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
autoCorrect={false}
keyboardType="email-address"
/>
<Text>Mật khẩu</Text>
<TextInput
accessibilityLabel="Mật khẩu"
style={styles.input}
value={password}
onChangeText={setPassword}
autoCapitalize="none"
secureTextEntry
/>
<Pressable
accessibilityRole="button"
style={styles.button}
onPress={() => onSubmit({email, password})}>
<Text style={styles.buttonLabel}>Đăng nhập</Text>
</Pressable>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
screen: {flex: 1},
content: {flexGrow: 1, padding: 24, justifyContent: 'center'},
input: {minHeight: 48, borderWidth: 1, padding: 12, marginBottom: 16},
button: {minHeight: 48, padding: 12, backgroundColor: '#0369a1'},
buttonLabel: {color: '#fff', textAlign: 'center'},
});
Các parent phía trên cũng phải có chiều cao hữu hạn hoặc truyền flex: 1 phù hợp. flexGrow của vùng nội dung cho phép form tận dụng khoảng trống, trong khi vẫn có khả năng cuộn nếu nội dung dài hơn viewport. Đừng đặt chiều cao cố định theo một thiết bị để ép form vừa màn hình.
Offset phải phản ánh bố cục thật
Theo tài liệu, keyboardVerticalOffset liên quan khoảng cách từ đầu màn hình tới view React Native. Header của navigator, safe area và cách màn hình cha đặt nội dung có thể tác động tới cấu hình cần dùng.
Hãy đo hoặc lấy thông tin bố cục từ lớp chịu trách nhiệm về header, rồi kiểm tra trên màn hình thực tế. Không cộng cùng một inset ở nhiều nơi. Nếu xuất hiện khoảng trắng lớn phía trên bàn phím, xem lần lượt header, padding, inset và cơ chế resize của cửa sổ thay vì giảm một con số ngẫu nhiên cho đến khi vừa mắt.
Ví dụ dùng padding trên iOS và height trên Android như điểm bắt đầu. Đó không phải bảo đảm rằng mọi app phải dùng cùng hai giá trị. Nếu hệ điều hành hoặc một lớp khác đã thay đổi kích thước viewport, cần kiểm tra tương tác để tránh bù khoảng trống hai lần.
Vì sao chọn handled cho thao tác chạm?
Khi bàn phím mở, hành vi mặc định của ScrollView có thể làm lần chạm ngoài ô nhập đóng bàn phím mà phần tử con không nhận lần chạm đó. handled cho phép phần tử con như nút xử lý tap theo quy tắc được tài liệu mô tả.
Đừng nhầm thuộc tính này với việc tự submit hoặc tự đóng bàn phím. Callback vẫn cần xử lý validation, trạng thái đang gửi và phản hồi lỗi. Ví dụ giữ phần đó ở ngoài để bạn tập trung kiểm tra bố cục mà không gắn mẫu đăng nhập vào một backend giả.
keyboardDismissMode="on-drag" cho phép thao tác kéo đóng bàn phím. Chế độ interactive là hành vi riêng iOS, nên không nên chọn nó rồi kỳ vọng Android hoạt động giống hệt. Nếu cần UX khác nhau giữa nền tảng, hãy cấu hình và ghi rõ mục tiêu kiểm tra.
Checklist kiểm tra trước khi đưa vào app
Thử form với màn hình nhỏ, font lớn, bàn phím email và bàn phím mật khẩu. Focus ô cuối, cuộn tới nút gửi, chạm nút khi bàn phím còn mở và đóng/mở bàn phím nhiều lần. Thử cả màn hình có header lẫn màn hình trình bày toàn màn hình nếu sản phẩm có hai kiểu đó.
Với form dài, kiểm tra riêng việc cuộn tới input đang focus; chỉ điều chỉnh kích thước view không tự chứng minh mọi input đều nằm đúng vị trí. Với màn hình nhiều lớp cuộn, hạn chế thêm ScrollView lồng nhau chỉ để che lỗi parent thiếu kích thước.
Ví dụ này chưa phải kết quả kiểm thử trên thiết bị cụ thể. Khi đánh giá, ghi lại phiên bản React Native, OS, cấu hình navigator và các bước tái hiện để team có thể kiểm tra cùng một tình huống.
Kết luận
Sửa form bị bàn phím che nên bắt đầu từ chiều cao parent, vùng cuộn và offset thực tế. Sau đó kiểm tra thao tác chạm và khác biệt nền tảng; cách này dễ duy trì hơn việc rải thêm padding cố định quanh từng ô nhập.




