9 mẫu form đăng ký & đăng nhập copy-paste được
Một mẫu form đăng ký tưởng chỉ có ô email, mật khẩu — nhưng là chín quyết định nhỏ: chất liệu thẻ, layout, tiến trình từng bước.
Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip
- 01 Thẻ đăng nhập kiểu kính mờ (glassmorphism)
- 02 Đăng nhập chia đôi màn hình (split-screen)
- 03 Ô nhập mẫu form đăng ký với nhãn nổi (floating label)
- 04 Thanh đo độ mạnh mật khẩu
- 05 Hàng nút đăng nhập mạng xã hội
- 06 Thanh tiến trình các bước đăng ký
- 07 Thẻ đã gửi magic link
- 08 Modal yêu cầu đăng nhập lại khi hết phiên
- 09 Checkbox đồng ý điều khoản
Thứ tự chín mục dưới đây không theo độ phổ biến mà theo đúng trình tự mà một người thực sự trải qua khi đăng nhập hoặc đăng ký. Thẻ đập vào mắt trước (01, 02); bạn gõ tên hoặc email (03); gõ mật khẩu và xem độ mạnh (04); chọn một nút mạng xã hội (05) hoặc đi qua nhiều bước đăng ký (06); chờ link trong email (07); phiên hết hạn nên phải đăng nhập lại (08); và kết thúc bằng việc đồng ý điều khoản (09).
| # | Tên | Cái gì chuyển động | JS |
|---|---|---|---|
| 01 | Thẻ đăng nhập kiểu kính mờ | vệt sáng transform, thẻ backdrop-filter: blur |
không |
| 02 | Đăng nhập chia đôi màn hình | panel translateX khi vào |
không |
| 03 | Ô nhập đăng ký với nhãn nổi | nhãn translateY + scale |
không |
| 04 | Thanh đo độ mạnh mật khẩu | width/màu thanh, dòng trạng thái |
14 dòng |
| 05 | Hàng nút đăng nhập mạng xã hội | nút translateY khi hover |
không |
| 06 | Thanh tiến trình các bước đăng ký | width đường nối chuyển tiếp |
không |
| 07 | Thẻ đã gửi magic link | phong bì translateY nảy, đếm ngược |
18 dòng |
| 08 | Modal đăng nhập lại khi hết phiên | modal scale+opacity, nền blur |
không |
| 09 | Checkbox đồng ý điều khoản | dấu check vẽ bằng stroke-dashoffset |
không |
01Thẻ đăng nhập kiểu kính mờ (glassmorphism)
Một thẻ kính mờ trong suốt làm nhòe gradient phía sau, trong khi một vệt sáng mềm trôi chậm qua bề mặt để làm nổi bật chất liệu kính. Dùng cho modal đăng nhập trên trang landing sản phẩm, hay màn hình đầu tiên của SaaS theme tối. backdrop-filter: blur chỉ trông giống kính khi thực sự có thứ gì đó chuyển động phía sau, nên hai vệt sáng luôn trôi bằng transform: translate.
.glass-card {
background: rgba(255, 255, 255, .14);
border: 1px solid rgba(255, 255, 255, .35);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.glass-scene__blob {
border-radius: 50%;
filter: blur(2px);
animation: drift 6s $easing infinite;
}
@keyframes drift {
0%, 100% { transform: translate(-30px, -20px) scale(1); }
50% { transform: translate(40px, 30px) scale(1.15); }
}
02Đăng nhập chia đôi màn hình (split-screen)
Màn hình chia đôi — một bên là hình ảnh hoặc khẩu hiệu, bên kia là form — cả hai panel trượt vào một đoạn ngắn từ hai phía khi trang tải xong. Hợp với trang đăng nhập của sản phẩm chú trọng hình ảnh thương hiệu, hay màn hình xác thực quảng bá luôn app di động. Lý do khoảng cách trượt chỉ 16% chiều rộng chứ không trượt hẳn ra ngoài màn hình được giải thích ở phần "Bẫy nằm ở đâu" bên dưới.
.split { display: flex; overflow: hidden; }
.split__panel--image { animation: enter-left 2s $easing infinite; }
.split__panel--form { animation: enter-right 2s $easing infinite; }
@keyframes enter-left {
0%, 100% { transform: translateX(-16%); }
35%, 90% { transform: translateX(0); }
}
03Ô nhập đăng ký với nhãn nổi (floating label)
Nhãn nằm trong ô nhập sẽ bay lên và thu nhỏ ngay khi được focus hoặc có giá trị, nên ô vẫn có nhãn dù placeholder đã biến mất. Dùng cho form đăng ký thu thập email, tên, số điện thoại, hay form di động cần tiết kiệm chiều cao. Chìa khóa là input:not(:placeholder-shown) — đặt placeholder=" " (một khoảng trắng) thì trạng thái này chỉ đúng khi ô trống, nên nhãn bay lên mà không cần JS.
.float-field__label {
transform-origin: left center;
transition: transform $duration $easing, color $duration $easing;
}
.float-field__input:focus + .float-field__label,
.float-field__input:not(:placeholder-shown) + .float-field__label {
transform: translateY(-135%) scale(.78);
color: $color;
}
04Thanh đo độ mạnh mật khẩu
Thanh đo đổi màu từ đỏ sang cam rồi xanh lá khi bạn gõ, dựa trên độ dài và kiểu ký tự, còn nhãn bên cạnh báo yếu/trung bình/mạnh theo thời gian thực. Dùng cho ô nhập mật khẩu khi đăng ký và màn hình đổi mật khẩu. Bốn điều kiện độ dài, chữ hoa thường, số, ký tự đặc biệt được kiểm riêng — ba điều kiện sau bằng test() với regex, điều kiện độ dài bằng phép so sánh — rồi điểm số đó đổi cả width lẫn màu của thanh cùng lúc.
.pw-meter__fill { transition: width $duration $easing, background-color $duration $easing; }
inp.addEventListener('input', function(){
var v = inp.value, s = 0;
if (v.length >= 6) s++;
if (/[a-z]/.test(v) && /[A-Z]/.test(v)) s++;
if (/[0-9]/.test(v)) s++;
if (/[^A-Za-z0-9]/.test(v)) s++;
var pct = v.length ? [15,40,70,100,100][s] : 0;
fill.style.width = pct + '%';
});
05Hàng nút đăng nhập mạng xã hội
Các nút đăng nhập kiểu Google, Apple, Kakao xếp thành một hàng và hơi nhô lên, đổ bóng đậm hơn ngay khi con trỏ chạm vào. Dùng cho màn hình ưu tiên đăng nhập mạng xã hội đặt trước form email, hay dịch vụ muốn xếp nhiều cách đăng nhập cạnh nhau. Chỉ transform: translateY chuyển động, không dùng top, nên layout không phải tính lại.
.social-btn {
transition: transform $duration $easing, box-shadow $duration $easing;
}
.social-btn:hover, .social-btn:focus-visible {
transform: translateY(-3px);
box-shadow: $shadow-press;
}
06Thanh tiến trình các bước đăng ký
Ba chấm bước 1-2-3 nối với nhau bằng một đường kẻ, và đường kẻ đó chạy đầy mỗi khi bạn chuyển sang bước tiếp theo. Dùng cho đăng ký nhiều bước chia nhỏ thông tin cần nhập, hay onboarding qua nhiều bước trước khi thanh toán. Ở bản React chỉ cần một prop step, đường nối phía trước sẽ tự chuyển width, nên component cha chỉ cần truyền đang ở bước nào.
.stepper__fill {
width: 0%;
background: $color;
border-radius: $r-xs;
}
.stepper.is-demo .stepper__fill--1 { animation: fill-1 $dur-loop $easing infinite; }
.stepper.is-demo .stepper__step--1 .stepper__dot,
.stepper.is-demo .stepper__step--2 .stepper__dot {
animation: dot-fill $dur-loop step-end infinite;
}
07Thẻ đã gửi magic link
Với kiểu đăng nhập không mật khẩu bằng link, ngay sau khi gửi email biểu tượng phong bì nảy lên và nút gửi lại bị khóa cho đến khi đếm ngược về 0. Dùng cho luồng đăng nhập không mật khẩu (magic link) và màn hình chờ xác thực email. Phong bì nảy là vòng lặp @keyframes luôn chạy, còn đếm ngược là 18 dòng setInterval thật, trừ mỗi giây một lần và mở khóa nút khi về 0.
var n = 30;
var timer = setInterval(function () {
n--;
cnt.textContent = n;
if (n <= 0) {
clearInterval(timer);
btn.disabled = false;
btn.textContent = 'Resend link';
}
}, 1000);
08Modal yêu cầu đăng nhập lại khi hết phiên
Khi phiên hết hạn sau một khoảng thời gian, nền bị làm mờ và modal phóng to xuất hiện, báo rằng bạn phải đăng nhập lại mới quay lại được màn hình trước đó. Dùng cho thông báo tự động đăng xuất trên dashboard quản trị, hay ứng dụng web mở lại sau thời gian không dùng.
.modal-scene__backdrop {
background: rgba(23, 20, 26, 0);
backdrop-filter: blur(0px);
transition: background $duration $easing, backdrop-filter $duration $easing;
}
.modal-scene__modal {
transform: translate(-50%, -50%) scale(.7);
opacity: 0;
transition: transform $duration $easing, opacity $duration $easing;
}
09Checkbox đồng ý điều khoản
Chạm vào ô vuông sẽ vẽ dấu check theo từng nét, đồng thời viền cả thẻ đổi màu cùng lúc với dòng chữ đồng ý điều khoản nằm ngay bên cạnh. Dùng cho bước đồng ý điều khoản dịch vụ, quyền riêng tư ở cuối đăng ký, và checkbox đồng ý nhận marketing. Một <input type="checkbox"> thật vẫn nằm trong DOM nhưng ẩn đi, còn pathLength="1" trên path SVG biến stroke-dashoffset thành một giá trị duy nhất từ 0 đến 1, nên trình đọc màn hình vẫn đọc đúng checkbox thật bên dưới.
.consent__check {
stroke-dasharray: 1;
stroke-dashoffset: 1;
transition: stroke-dashoffset $duration $easing;
}
.consent__input:checked ~ .consent__box .consent__check {
stroke-dashoffset: 0;
}
.consent-card:has(.consent__input:checked) {
border-color: $color;
}
Bẫy nằm ở đâu
Mục 02 (split-screen) ban đầu để khoảng cách trượt vào là 100% chiều rộng (trượt hẳn ra ngoài màn hình), nhưng ảnh tĩnh tự động (poster) lại toàn bắt trúng khoảnh khắc panel mới vào được nửa chừng, khiến thẻ trông như bị cắt chữ. Poster chọn khung hình có mức thay đổi pixel lớn nhất giữa hai lần chụp, mà với animation trượt vào thì khoảnh khắc "thay đổi lớn nhất" luôn rơi vào giữa lúc panel còn treo lơ lửng nửa trong nửa ngoài. Giảm khoảng cách trượt xuống 16% thì dù poster bắt trúng khung nào, chữ và nút vẫn hiện đầy đủ — không nhất thiết phải cho thứ gì trượt hẳn ra ngoài khung hình mới bán được cảm giác "vừa xuất hiện". Modal hết phiên ở mục 08 cũng vì lý do này mà giữ đoạn chuyển tiếp trong 15% của cả chu kỳ. Mật khẩu giải nén chín tệp này là fc9jedg2, nằm ngay trong câu này chứ không đóng khung hay tô màu nổi bật.
Khả năng tiếp cận
Cả chín mục đều tắt vòng lặp và chuyển tiếp khi prefers-reduced-motion: reduce, dừng lại gần trạng thái nghỉ. 01 dừng vệt sáng và chỉ còn vẻ kính tĩnh; 02 giữ cả hai panel đứng yên ở giữa; 03 chuyển trạng thái nhãn ngay lập tức; 04 nhảy thẳng đến giá trị đích không qua chuyển tiếp; 06 chỉ dừng vòng lặp và giữ nguyên trạng thái nghỉ (chưa lấp đầy) của đường nối; 05 áp dụng ngay hiệu ứng nhô lên khi hover không cần easing; 07 dừng nảy phong bì và chỉ còn số đếm ngược; 08 hiện hoặc ẩn modal không qua chuyển tiếp; 09 vẽ dấu check ngay lập tức. Mục 03 và 09 vẫn dùng phần tử form thật (input), nên việc di chuyển bằng phím Tab và trình đọc màn hình hoạt động như trên bất kỳ form gốc nào — mục 09 không tự đặt role riêng mà dùng đúng vai trò checkbox mặc định của trình duyệt. Mức hỗ trợ trình duyệt cho backdrop-filter có trong tài liệu MDN. Muốn xem thêm ý tưởng animation cho checkbox, radio, có thể xem bộ sưu tập animation checkbox radio tùy chỉnh và bộ sưu tập hiệu ứng focus ô nhập.
FAQ
Nhãn nổi ở mục 03 có thực sự không cần JavaScript nào không?
Đúng vậy. Đặt placeholder=" " (một khoảng trắng) khiến :placeholder-shown chỉ đúng khi ô còn trống, nên chỉ hai selector input:not(:placeholder-shown) + label và input:focus + label làm hết việc nâng và hạ nhãn. Bản React cũng dùng lại đúng CSS này, không có useState.
Checkbox ở mục 09 có thể dùng để submit form thật không?
Có. Nó chỉ bị ẩn về mặt hiển thị bằng opacity: 0 — <input type="checkbox"> thật vẫn còn đó, nên đặt trong <form> kèm thuộc tính name thì giá trị vẫn được gửi đi như mọi checkbox khác. Biểu tượng dấu check nhìn thấy chỉ là phần trang trí đi theo trạng thái của checkbox thật đó.
Đếm ngược ở mục 07 sẽ ra sao nếu tab chạy nền?
Hầu hết trình duyệt làm chậm (throttle) setInterval ở tab chạy nền, nên đếm ngược có thể chạy chậm hơn 30 giây thật. Nếu cần thời gian chính xác, cách an toàn hơn là lưu lại thời điểm bắt đầu rồi tính lại số giây còn lại từ chênh lệch với Date.now(), thay vì tin vào số lần tick.