GODRICH
#css-animation#button#loader#transition#scroll#ui#xu-huong-thiet-ke

9 Segmented Control UI — dán là chạy

Segmented control là nút chuyển tab hình viên thuốc mà bạn hay thấy trong màn hình cài đặt iOS, chọn một trong vài lựa chọn thì nền pill trượt sang đúng ô đó.

Bộ sưu tập dán-là-chạy này gom 9 kiểu UI chọn lựa — segmented control, chip lọc, reaction emoji, swatch màu và hơn thế — những thứ bạn chắc đã thấy trong app nhưng chưa biết gọi tên là gì.

Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip

Chín mục được xếp theo thứ tự gặp trên một màn hình, không theo độ phổ biến. 01–02 là segmented control chọn một trong vài chế độ xem, 03–04 là chip bấm để chọn tùy chọn, 05–06 là để lại reaction bằng emoji, 07 chọn màu, 08 chọn giờ, 09 chọn tiêu chí sắp xếp.

01Segmented control kiểu pill iOS

Bạn thấy gì

Ba radio chia đều chiều rộng, nền pill màu xanh trượt bằng translateX đến đúng nhãn đang được chọn. Giữa đường, pill giãn ra scaleX(1.25) rồi ổn định qua một cú bật nhẹ 1.05 — độ giãn thạch (jelly stretch) đưa cảm giác lại gần thêm một bước với độ nảy lò xo của iOS.

Segmented control kiểu pill iOS
Mở trong tab mới

01 · Segmented control kiểu pill iOS — Diện tích đo được 6.007% · khung hình chuyển động 15/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

translateX(N*100%)균등폭 flex0.22초

Dùng ở đâu

Hợp cho nút chuyển chế độ xem trong cài đặt, hoặc bộ lọc trạng thái đơn hàng (tất cả/đang xử lý/hoàn tất).

.seg__pill {
  position: absolute;
  width: calc((100% - 8px) / 3);
  transform: translateX(0);
  transition: transform $duration $easing;
}
#seg1-2:checked ~ .seg__track .seg__pill { transform: translateX(100%); }
#seg1-3:checked ~ .seg__track .seg__pill { transform: translateX(200%); }

02Segmented control 2 icon

Chuyện gì diễn ra trên màn hình

Chọn xem dạng danh sách hay lưới thì nền pill trượt đúng nửa track bằng translateX, chỉ icon được chọn đổi sang màu đậm. Pill cũng mang cùng độ giãn thạch như 01 — giãn scaleX khi di chuyển và bật nhẹ khi cập bến, khiến track như một vật liệu có độ đàn hồi.

Segmented control 2 icon
Mở trong tab mới

02 · Segmented control 2 icon — Diện tích đo được 4.462% · khung hình chuyển động 14/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

translateX 50%aria-label0.18초

Chỗ dùng

Đây là cơ chế đứng sau nút chuyển danh sách/lưới trên nhiều bảng tin.

.iv__pill {
  position: absolute;
  width: $sp-12;
  transform: translateX(0);
  transition: transform $duration $easing;
}
#iv1-2:checked ~ .iv__track .iv__pill { transform: translateX(100%); }

03Chip lọc chọn nhiều

Chuyển động thế nào

Mỗi chip là một checkbox; chọn nhiều chip cùng lúc thì nền từng chip được tô từ giữa ra bằng scaleX và icon tick SVG nội tuyến bung từ scale(0) lên 1. Cả phần tô lẫn dấu tick đều có keyframe bật quá đà: nền tô tràn ra một nhịp rồi thu về, dấu tick bật vượt 1 lên 1.2 rồi mới ổn định.

Chip lọc chọn nhiều
Mở trong tab mới

03 · Chip lọc chọn nhiều — Diện tích đo được 3.999% · khung hình chuyển động 6/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaleX 채움:has() 선택자0.2초

Dùng khi nào

Hợp cho bộ lọc danh sách sản phẩm như miễn phí ship, giao trong ngày, giảm giá.

.cf__chip::before {
  content: "";
  position: absolute; inset: 0;
  background: $color;
  transform: scaleX(0);
  transform-origin: center;
  transition: transform $duration $easing;
}
.cf__chip:has(.cf__input:checked)::before { transform: scaleX(1); }

04Chip radio chọn size

Bạn thấy gì

Chọn một trong các chip radio S/M/L/XL thì chip đó lún nhẹ về scale(.84) rồi bật lại, viền quanh chip dày lên. Cú bật lại đi qua điểm 1.05 của độ nảy lò xo, tạo cảm giác đàn hồi thật như ngón tay vừa nhấn xuống chip.

Chip radio chọn size
Mở trong tab mới

04 · Chip radio chọn size — Diện tích đo được 2.096% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scale 바운스cubic-bezier 오버슈트0.3초

Dùng ở đâu

Cùng kiểu với chọn size sản phẩm hoặc chọn số người khi đặt chỗ.

.sz__chip:has(.sz__input:checked) {
  border-color: $color;
  border-width: 3px;
  animation: sz-press $duration $easing;
}
@keyframes sz-press {
  0%   { transform: scale(1); }
  35%  { transform: scale(.84); }
  70%  { transform: scale(1.05); }
  100% { transform: scale(1); }
}

05Bảng chọn reaction emoji popup

Chuyện gì diễn ra trên màn hình

Nhấn nút react thì một dải bốn emoji hiện lên bằng opacity và translateY, aria-expanded của nút được cập nhật ngay. Trên đường hiện lên, dải emoji bị ép dẹt theo chiều dọc rồi tràn ngang quá đà — một cú lắc kiểu thạch, như miếng sticker mềm bật ra.

Bảng chọn reaction emoji popup
Mở trong tab mới

05 · Bảng chọn reaction emoji popup — Diện tích đo được 6.434% · khung hình chuyển động 7/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

opacity+translateYaria-expandedEsc 로 닫힘

Chỗ dùng

Đây là cơ chế đứng sau việc thả reaction vào tin nhắn hay bài đăng.

.rp__pop {
  transform: translate(-50%, 8px) scale(.85);
  opacity: 0;
  pointer-events: none;
  transition: transform $duration $easing, opacity $duration $easing;
}
.rp.is-open .rp__pop {
  transform: translate(-50%, 0) scale(1);
  opacity: 1;
  pointer-events: auto;
}

06Emoji bật nảy kèm số đếm

Chuyển động thế nào

Chọn một emoji thì emoji đó bật scale lớn lên, bốn chấm nhỏ văng ra ngoài rồi mờ dần, số trên badge bên cạnh tăng thêm 1. Cú bật này chính là một độ giãn thạch — trục x và y so le nhau giãn và nén, emoji loang sang hai bên ở đỉnh rồi bật ngược theo chiều dọc khi trở về.

Emoji bật nảy kèm số đếm
Mở trong tab mới

06 · Emoji bật nảy kèm số đếm — Diện tích đo được 0.487% · khung hình chuyển động 9/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scale 바운스burst dot ×4aria-live

Dùng khi nào

Cùng thủ thuật đứng sau nút thả tim trên video ngắn.

.bc.is-play .bc__emoji { animation: bc-emoji $duration $easing; }
.bc.is-play .bc__burst { animation: bc-burst $duration $easing; }
@keyframes bc-emoji {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.5); }
  60%  { transform: scale(.9); }
  100% { transform: scale(1); }
}

07Lưới chọn màu swatch

Bạn thấy gì

Sáu mẫu màu xếp lưới 3×2, chọn một mẫu thì một badge tick nhỏ ở góc phóng từ scale(0) lên 1. Badge bật lên 1.25 với một độ nghiêng nhẹ rồi mới ổn định, ghim khoảnh khắc được chọn vào đầu ngón tay.

Lưới chọn màu swatch
Mở trong tab mới

07 · Lưới chọn màu swatch — Diện tích đo được 0.578% · khung hình chuyển động 7/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scale(0)→scale(1) 체크3×2 grid0.22초

Dùng ở đâu

Hợp cho lưới tùy chọn màu sản phẩm hoặc màn hình chọn màu theme.

.cw__check {
  transform: scale(0);
  transition: transform $duration $easing;
}
.cw__check-icon {          /* Phosphor SVG nội tuyến — kế thừa currentColor */
  position: absolute; inset: 3px;
  color: $color;
}
.cw__cell:has(.cw__input:checked) .cw__check { transform: scale(1); }

08Bộ chọn giờ kiểu odometer

Chuyện gì diễn ra trên màn hình

Nhấn nút lên/xuống ở ô giờ/phút thì số hiện tại trượt ra bằng translateY, số kế tiếp trượt vào đúng chỗ đó, giống đồng hồ công-tơ-mét ô tô lật số. Sau khi vào chỗ, con số giãn chiều cao lên 1.15 rồi lún nhẹ — một cú cập bến kiểu thạch mô phỏng phản lực bánh răng của đồng hồ cơ.

Bộ chọn giờ kiểu odometer
Mở trong tab mới

08 · Bộ chọn giờ kiểu odometer — Diện tích đo được 0.499% · khung hình chuyển động 7/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

translateY 롤오버data-dir 버튼0.18초

Chỗ dùng

Dùng để chọn giờ đặt lịch hay đặt giờ báo thức.

function roll(col, dir) {
  var win = col.querySelector('.tp__win');
  var cur = win.querySelector('.is-cur'), next = win.querySelector('.is-next');
  var v = (parseInt(cur.textContent, 10) + dir + 60) % 60;
  next.textContent = String(v).padStart(2, '0');
  cur.classList.add('is-leaving');
  next.classList.add('is-arriving');
}

09Dropdown sắp xếp có dấu tick

Chuyển động thế nào

Mở nút sắp xếp thì danh sách hiện ra bằng opacity và translateY, chỉ tùy chọn được chọn có dấu tick, chữ trên nút cũng đổi theo tên tùy chọn đó. Danh sách mở qua độ bật quá đà theo chiều dọc (0.9 lên 1.04), còn dấu tick bật vào từ 0 đến 1.3.

Dropdown sắp xếp có dấu tick
Mở trong tab mới

09 · Dropdown sắp xếp có dấu tick — Diện tích đo được 8.764% · khung hình chuyển động 11/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

role=menuopacity+translateY0.2초

Dùng khi nào

Dùng để sắp xếp danh sách sản phẩm hay bài viết trên bảng tin.

.sd__menu {
  transform: translate(-50%, -6px);
  opacity: 0;
  transition: transform $duration $easing, opacity $duration $easing;
}
.sd.is-open .sd__menu { transform: translate(-50%, 0); opacity: 1; }
.sd__check {               /* Dấu tick Phosphor SVG nội tuyến */
  color: $color;
  transform: scale(0);
  transition: transform $dur-instant $easing;
}
.sd__item.is-checked .sd__check { transform: scale(1); }

Dễ hỏng ở đâu — bẫy input ẩn và :has()

Input ẩn về mặt thị giác

01, 02, 03, 04, 07 chỉ ẩn <input> thật về mặt thị giác bằng position: absolute; width: 1px; height: 1px; clip: rect(0,0,0,0);. Nếu dùng display: none hay visibility: hidden thì phần tử sẽ bị loại hoàn toàn khỏi thứ tự focus, người chỉ dùng bàn phím sẽ không bao giờ chạm tới được bằng Tab. Selector :has() mà 03 và 07 dùng chỉ chạy trên Chrome/Edge 105+, Safari 15.4+, Firefox 121+ — trên bản cũ hơn dấu tick đơn giản là không hiện, còn việc bấm chọn vẫn hoạt động bình thường bên dưới. Mật khẩu giải nén của zip chứa cả chín file này là e9nu36x4, và hiệu ứng lật số của 08 cần đoạn script roll() 12 dòng vì CSS một mình không thể vừa đổi chữ vừa trượt hai lớp số cùng lúc đúng nhịp. Các popup mở bằng nút bấm như 05 và 09 lẽ ra cũng phải đóng khi bấm ra ngoài hoặc nhấn Esc — demo này chỉ nối Esc cho gọn, còn đóng khi bấm ngoài thì bị bỏ, nên thêm một listener click trên document trước khi đưa vào dự án thật.

Khả năng tiếp cận

Trình đọc màn hình và input thật

01, 02, 03, 04, 07 giữ <input> thật ẩn nhưng vẫn tồn tại, nên trình đọc màn hình đọc đúng radio hay checkbox, và Tab cùng Space hoặc phím mũi tên đổi được trạng thái. Nút mở popup và menu ở 05, 09 cập nhật aria-expanded mỗi lần mở đóng, còn từng tùy chọn ở 09 mang role="menuitemradio" cùng aria-checked để báo lựa chọn hiện tại. Bộ đếm ở 06 dùng aria-live="polite", nên khi số phản ứng đổi, trình đọc màn hình lặng lẽ đọc số mới. Dưới prefers-reduced-motion: reduce, cả chín mục tắt transition và animation nhưng vẫn giữ nguyên trạng thái đã đến — pill, chip, menu — nên thông tin trạng thái không biến mất cùng chuyển động.

@media (prefers-reduced-motion: reduce) {
  .seg__pill, .cf__chip::before, .cw__check { transition: none; }
  .bc.is-play .bc__emoji, .bc.is-play .bc__burst { animation: none !important; }
}

Các mẫu UI thực chiến khác cũng có ở danh mục UI, còn cách trang này kiểm chứng từng demo trước khi đăng thì có ở trang giới thiệu.

FAQ

Segmented control khác tab ở điểm nào?

Tab thường chuyển sang trang hay vùng nội dung khác khi bấm, còn segmented control chỉ đổi một giá trị như bộ lọc hay chế độ xem, ngay trong cùng một màn hình. Đó là lý do 01 và 02 đổi trạng thái ngay lập tức mà không chuyển trang.

Không có :has() thì 03, 04, 07 có hỏng hoàn toàn không?

Không. Thiếu :has() thì dấu tick hay vòng ring chỉ đơn giản là không hiện ra, còn trạng thái :checked của <input> thật và giá trị gửi form vẫn hoạt động bình thường. Chỉ mất phản hồi thị giác, không mất chức năng.

Bản React của 09 có đóng bằng Esc không?

Có — component trong react/09-sort-dropdown-checkmark-menu cũng gắn cùng listener onKeyDown, nhấn Esc thì open chuyển về false và menu đóng lại. Đóng bằng bấm ra ngoài cũng bị bỏ qua ở đây như phần bẫy đã nói, nên thêm phần đó trước khi dùng cho dự án thật.

Nhập mật khẩu file nén

Mật khẩu nằm ngay trong bài viết này. Đọc tới đó bạn sẽ thấy.