GODRICH

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

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. 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).

translateX(N*100%)균등폭 flex0.22초
.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

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

translateX 50%aria-label0.18초
.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

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. 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á.

scaleX 채움:has() 선택자0.2초
.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

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ùng kiểu với chọn size sản phẩm hoặc chọn số người khi đặt chỗ.

scale 바운스cubic-bezier 오버슈트0.3초
.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

Nhấn nút react thì một dải bốn emoji hiện lên bằng opacitytranslateY, aria-expanded của nút được cập nhật ngay. Đây là cơ chế đứng sau việc thả reaction vào tin nhắn hay bài đăng.

opacity+translateYaria-expandedEsc 로 닫힘
.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

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ùng thủ thuật đứng sau nút thả tim trên video ngắn.

scale 바운스burst dot ×4aria-live
.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

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. 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.

scale(0)→scale(1) 체크3×2 grid0.22초
.cw__check {
  transform: scale(0);
  transition: transform $duration $easing;
}
.cw__check-icon {          /* 인라인 Phosphor SVG — 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

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ố. Dùng để chọn giờ đặt lịch hay đặt giờ báo thức.

translateY 롤오버data-dir 버튼0.18초
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

Mở nút sắp xếp thì danh sách hiện ra bằng opacitytranslateY, 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 đó. Dùng để sắp xếp danh sách sản phẩm hay bài viết trên bảng tin.

role=menuopacity+translateY0.2초
.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 {               /* 인라인 Phosphor SVG 체크 */
  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()

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

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.