Cập nhật lần cuối:

Thiết kế xác thực số ký tự nhập liệu biểu mẫu - Triển khai giới hạn mà không ảnh hưởng UX

Khoảng 10 phút đọc

Xác thực số ký tự biểu mẫu là thách thức thiết kế cần cân bằng giữa duy trì tính toàn vẹn dữ liệu và bảo toàn trải nghiệm người dùng. Chỉ đặt thuộc tính maxlength là chưa đủ - cần xem xét bộ đếm thời gian thực, xử lý cặp thay thế, xác thực phía máy chủ và phản hồi phù hợp khi xảy ra lỗi. Bài viết này trình bày các mẫu thiết kế xác thực thực tế, bao gồm sự phù hợp với độ dài VARCHAR cơ sở dữ liệu. Hãy nắm vững khái niệm cơ bản về giới hạn ký tự trước khi đọc tiếp.

Cạm bẫy của thuộc tính maxlength

Thuộc tính maxlength của HTML là cách đơn giản nhất để giới hạn số ký tự. Tuy nhiên, nó có một số vấn đề mà nhà phát triển thường bỏ qua.

Vấn đề lớn nhất là maxlength giới hạn theo đơn vị mã UTF-16. Ký tự trong Mặt phẳng đa ngôn ngữ cơ bản (BMP) dùng 1 đơn vị mã, nhưng emoji và ký tự cặp thay thế tiêu thụ 2 đơn vị mã. Nghĩa là trong trường có maxlength="10", người dùng chỉ có thể nhập 5 emoji về mặt trực quan.

Loại ký tựĐơn vị mã UTF-16Tiêu thụ maxlengthVí dụ
Ký tự ASCII11A, 1, @
Tiếng Nhật (BMP)11あ, 漢, カ
Emoji cơ bản22😀, 🎉, ❤️
Emoji chuỗi ZWJ6-11 (thay đổi theo số thành phần)Giống cột bên🏳️‍🌈 là 6, 👨‍👩‍👧‍👦 là 11
Emoji cờ quốc gia44🇯🇵, 🇺🇸
CJK Thống nhất Hán tự Mở rộng B22𠮷 (tsuchiyoshi)

Vấn đề này đặc biệt nổi bật ở trường nhập tên. Một số chữ Hán đăng ký trong hộ tịch Nhật Bản thuộc CJK Thống nhất Hán tự Mở rộng B và tiêu thụ 2 đơn vị mã dưới maxlength. Họ "𠮷田" trông như 2 ký tự nhưng tiêu thụ 3 đơn vị mã. Điều này liên quan chặt chẽ đến vấn đề đếm ký tự toàn chiều rộng/nửa chiều rộng.

Vấn đề khác là maxlength từ chối nhập liệu một cách im lặng. Khi đạt giới hạn, không thể nhập thêm ký tự nhưng không có phản hồi giải thích lý do. Người dùng có thể nghi ngờ bàn phím hỏng hoặc lỗi trình duyệt.

Đếm ký tự chính xác bằng JavaScript

Để tránh vấn đề UTF-16 của maxlength, cần triển khai đếm theo cụm tự vị (Grapheme Cluster) trong JavaScript. Cụm tự vị là đơn vị mà con người nhận thức là "một ký tự", đếm chính xác cặp thay thế, ký tự kết hợp và chuỗi ZWJ là một ký tự đơn.

Phương pháp đáng tin cậy nhất là sử dụng API Intl.Segmenter.

// Đếm cụm tự vị bằng Intl.Segmenter
function countGraphemes(text) {
  const segmenter = new Intl.Segmenter('ja', { granularity: 'grapheme' });
  return [...segmenter.segment(text)].length;
}

// Ví dụ sử dụng
countGraphemes('Hello');        // 5
countGraphemes('こんにちは');    // 5
countGraphemes('👨‍👩‍👧‍👦');          // 1
countGraphemes('🇯🇵');           // 1
countGraphemes('𠮷田太郎');      // 4

Intl.Segmenter được hỗ trợ từ Chrome 87 trở lên và Firefox 125 trở lên, còn Safari cũng đã hỗ trợ (tính đến tháng 8 năm 2026). Ranh giới trên thực tế là Firefox, vì bản hỗ trợ đến muộn ở phiên bản 125 vào mùa xuân 2024, nên những môi trường còn dùng phiên bản cũ hơn sẽ phát sinh ngoại lệ. Khi triển khai, hãy kiểm tra sự tồn tại của API bằng typeof Intl.Segmenter === 'function' và chuyển sang thư viện dự phòng như grapheme-splitter nếu không có.

Tuy nhiên, không phải mọi biểu mẫu đều cần đếm cụm tự vị. Các trường chỉ chấp nhận ký tự ASCII như địa chỉ email hoặc URL hoạt động tốt với maxlength. Đếm cụm tự vị chỉ cần cho các trường người dùng nhập văn bản tự do (tên, bình luận, giới thiệu bản thân, v.v.).

Thiết kế bộ đếm ký tự thời gian thực

Bộ đếm ký tự thời gian thực là thành phần UI phản hồi trực quan số ký tự còn lại cho người dùng. Chỉ báo tiến trình hình tròn được sử dụng trong giới hạn ký tự của X (trước đây là Twitter) thể hiện lượng còn lại bằng độ dài của cung tròn, một thiết kế giúp người dùng cảm nhận được mức độ gần tới giới hạn mà không cần đọc con số.

Yếu tố thiết kếMẫu khuyến nghịMẫu nên tránhLý do
Định dạng hiển thị"Còn 42 ký tự" hoặc "158/200"Chỉ "Đã nhập 158 ký tự"Số còn lại thúc đẩy hành động người dùng tốt hơn
Vị tríGóc dưới bên phải trường nhậpPhía trên trường hoặc xaGiảm thiểu di chuyển mắt
Thay đổi màuVàng khi còn 20%, đỏ khi hếtLuôn cùng màuTruyền tải mức độ khẩn cấp bằng thị giác
Hành vi vượt giới hạnTô đỏ phần vượt + hiển thị số âmChặn nhập im lặngCho người dùng không gian chỉnh sửa
Khả năng tiếp cậnThông báo số còn lại bằng aria-live="polite"Chỉ hiển thị trực quanCung cấp thông tin cho người dùng trình đọc màn hình

Thiết kế của X xuất sắc vì chỉ báo hình tròn đổi màu khi tiến gần giới hạn, và khi vượt quá, số âm hiển thị bằng màu đỏ. Thay vì chặn nhập, nó cho phép tiếp tục chỉnh sửa và vô hiệu hóa nút đăng, cho người dùng thời gian quyết định cắt bỏ phần nào.

Sự cần thiết của xác thực phía máy chủ

Xác thực phía máy khách tồn tại vì mục đích UX và không đảm bảo bảo mật hay tính toàn vẹn dữ liệu. Cả thuộc tính maxlength và bộ đếm JavaScript đều có thể dễ dàng vô hiệu hóa qua công cụ phát triển trình duyệt. Gọi API trực tiếp hoàn toàn bỏ qua xác thực frontend.

Xác thực số ký tự phía máy chủ, giống như bảo mật độ dài mật khẩu, là tuyến phòng thủ cuối cùng. Hãy đảm bảo triển khai các điểm sau:

# Ví dụ xác thực phía máy chủ bằng Python
import unicodedata

def validate_text_length(text, max_chars=200, max_bytes=800):
    # Loại bỏ ký tự điều khiển
    cleaned = ''.join(c for c in text if unicodedata.category(c) != 'Cc')
    # Chuẩn hóa NFC
    normalized = unicodedata.normalize('NFC', cleaned.strip())
    # Kiểm tra số ký tự
    char_count = len(normalized)
    # Kiểm tra số byte UTF-8
    byte_count = len(normalized.encode('utf-8'))

    if char_count > max_chars:
        return False, f'Số ký tự vượt giới hạn ({char_count}/{max_chars})'
    if byte_count > max_bytes:
        return False, f'Kích thước dữ liệu vượt giới hạn'
    return True, None

Có một điểm cần lưu ý ở đây: len() trong ví dụ này đếm theo điểm mã chứ không phải theo cụm tự vị. Với cùng một chuỗi nhập 👨‍👩‍👧‍👦, cách đếm theo cụm tự vị cho ra 1, đếm theo điểm mã cho ra 7, còn đếm theo đơn vị mã UTF-16 cho ra 11 - tức là ba câu trả lời khác nhau cho cùng câu hỏi "chuỗi này dài bao nhiêu ký tự". Nếu chỉ khớp con số giới hạn mà để đơn vị đếm không thống nhất, người dùng sẽ gặp lỗi rất khó giải thích: trên màn hình vẫn còn trong giới hạn, nhưng khi gửi thì máy chủ từ chối. Cách an toàn hơn là chọn một trong hai chính sách - lấy đơn vị đếm phía máy chủ làm chuẩn rồi triển khai đúng cách đếm đó ở phía máy khách, hoặc để phía máy khách đếm theo cụm tự vị và chừa dư giới hạn phía máy chủ - sau đó ghi rõ đơn vị đã chọn như một phần của đặc tả xác thực.

Mẫu triển khai theo framework

FrameworkThư viện xác thựcTriển khai giới hạn ký tựTích hợp bộ đếm thời gian thực
ReactReact Hook Form + ZodĐịnh nghĩa schema bằng .max() của ZodTheo dõi giá trị nhập bằng watch() và hiển thị đếm
VueVeeValidate + YupĐịnh nghĩa schema bằng .max() của YupĐếm từ giá trị phản ứng v-model
AngularReactive FormsValidators.maxLength()Đếm qua Observable valueChanges
SvelteSuperforms + ZodĐịnh nghĩa schema bằng .max() của ZodĐếm bằng khai báo phản ứng $:

Thiết kế thông báo lỗi cho giới hạn ký tự

MẫuVí dụ thông báoĐánh giá
Chỉ nêu vấn đề"Vượt giới hạn ký tự"Trung bình - không rõ vượt bao nhiêu
Vấn đề + trạng thái"523/500 ký tự - vượt 23 ký tự"Tốt - số vượt rõ ràng
Vấn đề + giải pháp"Vượt 23 ký tự. Vui lòng xóa phần không cần thiết"Tốt - hành động tiếp theo rõ ràng
Cảnh báo tiến triểnVàng khi còn 50, đỏ khi vượt + thông báoXuất sắc - cảnh báo trước và hướng dẫn cụ thể sau khi vượt

Phương pháp cảnh báo tiến triển hiệu quả nhất. Cung cấp phản hồi trực quan khi tiến gần giới hạn và trình bày số vượt cụ thể cùng giải pháp khi vượt quá.

Textarea tự động thay đổi kích thước và giới hạn ký tự

Textarea tự động thay đổi kích thước mở rộng chiều cao tự động theo nội dung nhập. Khi kết hợp với giới hạn ký tự, cần một số quyết định thiết kế. Không đặt chiều cao tối đa có thể phá vỡ bố cục trang. Với trường giới hạn 500 ký tự, đặt tối đa khoảng 400px là thực tế. Thuộc tính CSS field-sizing: content cho phép tự động thay đổi kích thước textarea mà không cần JavaScript. Thuộc tính này được triển khai trong Chrome 123 năm 2024, và chỉ báo khả dụng đa trình duyệt của MDN (Baseline) đã chuyển nó sang trạng thái "mới khả dụng" vào tháng 6 năm 2026. Nói cách khác, nó hoạt động trên bản mới nhất của từng trình duyệt, nhưng bị bỏ qua ở những môi trường còn dùng phiên bản cũ hơn một chút. Vì khi không được hỗ trợ thì field-sizing chỉ đơn giản là không có tác dụng chứ không gây lỗi bố cục, cách làm thực tế là dùng hai lớp: giữ phần điều chỉnh chiều cao bằng JavaScript làm nền tảng và để thuộc tính này đảm nhiệm trên các trình duyệt có hỗ trợ.

Giới hạn ký tự biểu mẫu di động - Thách thức riêng

// Kiểm soát xác thực trong quá trình soạn IME
let isComposing = false;

textarea.addEventListener('compositionstart', () => {
  isComposing = true;
});

textarea.addEventListener('compositionend', () => {
  isComposing = false;
  validateLength(textarea.value);
});

textarea.addEventListener('input', () => {
  if (!isComposing) {
    validateLength(textarea.value);
  }
  updateCounter(textarea.value);
});

Thiết kế phù hợp với cơ sở dữ liệu

Sự không phù hợp giữa giới hạn ký tự frontend và định nghĩa cột cơ sở dữ liệu gây ra cắt ngắn dữ liệu hoặc lỗi trong môi trường sản xuất. Như trình bày chi tiết trong thiết kế độ dài VARCHAR cơ sở dữ liệu, VARCHAR(255) của MySQL dựa trên ký tự, nhưng tiêu thụ lưu trữ thực tế phụ thuộc vào mã hóa.

Cơ sở dữ liệuĐơn vị VARCHAR100 ký tự tiếng NhậtPhù hợp giới hạn frontend
MySQL (utf8mb4)Ký tựVừa VARCHAR(100)Dễ khớp với giới hạn ký tự frontend
PostgreSQLKý tựVừa VARCHAR(100)Dễ khớp với giới hạn ký tự frontend
SQL ServerKý tự (NVARCHAR)Vừa NVARCHAR(100)Dễ khớp với giới hạn ký tự frontend
OracleByte (mặc định)Cần VARCHAR2(300)Cần chuyển đổi byte
DynamoDBKích thước mục (400 KB)Không giới hạn theo thuộc tínhĐặt giới hạn ở tầng ứng dụng

Thực hành thiết kế an toàn là đặt giới hạn ký tự frontend nghiêm ngặt hơn định nghĩa cột cơ sở dữ liệu. Ví dụ, nếu cơ sở dữ liệu là VARCHAR(500), đặt giới hạn frontend khoảng 450 ký tự, tạo bộ đệm cho thay đổi số ký tự từ chuẩn hóa Unicode và cắt bỏ.

Chia sẻ bài viết này