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

9 CSS Toggle Switches — dán 1 dòng là chạy

CSS toggle switch là control chỉ dùng một input checkbox để bật tắt thật, còn track và núm là lớp vỏ CSS phủ lên trên.

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

Chín kiểu này không xếp theo độ hoành tráng mà theo việc track thật sự đổi cái gì. Mở đầu là hai kiểu chỉ dời vị trí núm (01, 02), tiếp đến hai kiểu đổi nội dung bên trong track (03, 04), rồi ba kiểu thêm một lớp diễn xuất như bị bóp hay bị lan màu (05 đến 07), và khép lại bằng hai kiểu — một dựng từ nhiều biến thể, một hoàn toàn không đụng được vào (08, 09). Khối code chỉ giữ đúng bộ chọn cốt lõi của mỗi switch, bản đầy đủ dán vào là chạy nằm trong file zip.

01Trượt cơ bản

Bạn thấy gì

Khi checkbox nhận checked, núm tròn trượt sang bên phải track bằng translateX, còn track lật giữa hai lớp nền chồng nhau chỉ qua opacity để đổi màu chứ không animate background-color trực tiếp. Ngay trước khi đến chỗ, núm vọt quá mốc một nhịp rồi hất lại kèm hiệu ứng squash dẹt xuống trong tích tắc, và bóng của track đậm lên rồi nhạt dần đúng lúc núm đáp xuống.

Trượt cơ bản
Mở trong tab mới

01 · Trượt cơ bản — Diện tích đo được 0.917% · 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:checked0.25초

Dùng ở đâu

Đây là dạng cơ bản nhất để thay checkbox trong màn hình cài đặt.

.switch__bg--off { background: rgba(255, 255, 255, .18); opacity: 1; }
.switch__bg--on  { background: $color; opacity: 0; }
.switch__knob { transform: translateX(0); transition: transform $duration $easing; }

.switch__input:checked ~ .switch__track .switch__bg--off { opacity: 0; }
.switch__input:checked ~ .switch__track .switch__bg--on  { opacity: 1; }
.switch__input:checked ~ .switch__track .switch__knob    { transform: translateX(24px); }

02Kiểu iOS

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

Track bo tròn xanh phối núm trắng đúng kiểu app Cài đặt của iOS, còn núm nảy vượt qua mốc translateX(24px) một nhịp rồi mới lùi lại đúng chỗ nhờ các mốc keyframes lệch nhau. Đỉnh điểm của cú nảy, núm bị ép dẹt theo chiều dọc bởi squash, còn bóng inset của track đậm lên khi núm đè xuống và nhạt ra khi nó rời đi.

Kiểu iOS
Mở trong tab mới

02 · Kiểu iOS — Diện tích đo được 1.118% · 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

cubic-bezier 스프링그림자0.3초

Chỗ dùng

Hợp với màn hình cài đặt web di động, nơi cảm giác phản hồi dưới ngón tay quan trọng.

@keyframes knob-loop {
  0%, 15%   { transform: translateX(0); }
  38%       { transform: translateX(28px); }   // nảy quá đà kiểu lò xo
  40%, 60%  { transform: translateX(24px); }
  85%, 100% { transform: translateX(0); }
}

03Nhãn ON/OFF gắn sẵn

Chuyển động thế nào

Track cố định sẵn chữ ON và OFF ở hai đầu, còn núm mang z-index cao hơn nên khi trượt qua sẽ che mất chữ ở phía nó vừa tới. Cuốt hành trình, núm nảy lên rồi bị ép dẹt bởi cú squash spring trước khi đứng hẳn, và bóng track đậm thêm đúng khoảnh khắc nó đè lên chữ.

Nhãn ON/OFF gắn sẵn
Mở trong tab mới

03 · Nhãn ON/OFF gắn sẵn — Diện tích đo được 1.507% · khung hình chuyển động 16/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

z-indexoverflow:hidden0.25초

Dùng khi nào

Hợp với switch trạng thái trên dashboard quản trị, nơi giá trị hiện tại cần đọc được bằng chữ chứ không chỉ bằng màu.

.switch__text--on  { left: 10px; }
.switch__text--off { right: 10px; }
.switch__knob {
  position: absolute; top: 3px; left: 3px; z-index: 1;
  transition: transform $duration $easing;
}
.switch__input:checked ~ .switch__track .switch__knob {
  transform: translateX(44px);
}

04Đổi icon

Bạn thấy gì

Núm trượt sang bên phải đúng lúc icon bên trong đổi từ dấu X sang dấu tick, cả hai icon nằm chồng lên nhau ở cùng một vị trí và chỉ hoán đổi opacity cho nhau. Quãng trượt vọt quá đích một nhịp rồi hất về nhờ spring, kèm một nhịp squash ngắn, và bóng track đậm lên đúng lúc hai icon đổi chỗ cho nhau.

Đổi icon
Mở trong tab mới

04 · Đổi icon — Diện tích đo được 1.05% · khung hình chuyển động 16/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

opacity 크로스페이드SVG 아이콘0.2초

Dùng ở đâu

Hợp với switch cho phép hoặc từ chối quyền, nơi kết quả cần được nói rõ bằng một icon duy nhất.

.switch__knob { transform: translateX(0); transition: transform $duration $easing; }
.switch__icon--x     { opacity: 1; }
.switch__icon--check { opacity: 0; }

.switch__input:checked ~ .switch__track .switch__knob        { transform: translateX(26px); }
.switch__input:checked ~ .switch__track .switch__icon--x     { opacity: 0; }
.switch__input:checked ~ .switch__track .switch__icon--check { opacity: 1; }

05Hiệu ứng bóp khi nhấn

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

Trong lúc con trỏ còn nhấn xuống, biến --sx kéo núm giãn ngang qua scaleX, còn --sy ép nó dẹt lại theo chiều cao; buông tay thì cả hai biến trở về 1 nên núm bật lại thành hình tròn. Cú bật vọt quá kích thước nghỉ một nhịp rồi mới thu vào, và bóng track đậm lúc bị nhấn sẽ nhạt dần theo ngay sau đó.

Hiệu ứng bóp khi nhấn
Mở trong tab mới

05 · Hiệu ứng bóp khi nhấn — Diện tích đo được 0.917% · khung hình chuyển động 13/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaleX:active0.15초

Chỗ dùng

Hợp với form di động ưu tiên chạm, nơi cần một tín hiệu tức thì báo rằng ngón tay đã chạm đúng chỗ.

.switch__knob {
  --x: 0px; --sx: 1; --sy: 1;
  transform: translateX(var(--x)) scaleX(var(--sx)) scaleY(var(--sy));
  transition: transform $duration $easing;
}
.switch__input:checked ~ .switch__track .switch__knob { --x: 24px; }
.switch:active .switch__knob { --sx: 1.35; --sy: .8; }

06Chuyển màu nền lan tỏa

Chuyển động thế nào

Nền track không đổi màu ngay lập tức mà một vòng tròn clip-path mở rộng dần từ đúng vị trí núm đứng, lan ra tới khi phủ gần hết track. Đúng lúc phủ xong, núm nảy nhẹ kèm squash báo hiệu đã chuyển xong, còn bóng track chuyển từ nông sang sâu đánh dấu hai trạng thái.

Chuyển màu nền lan tỏa
Mở trong tab mới

06 · Chuyển màu nền lan tỏa — Diện tích đo được 1.033% · khung hình chuyển động 16/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

clip-path:circle()번짐0.4초

Dùng khi nào

Hợp với card cài đặt mang cảm giác cao cấp hoặc switch demo trên trang landing, nơi bản thân khoảnh khắc chuyển đổi cũng đáng để xem.

.switch__fill {
  background: $color;
  clip-path: circle(0% at 18% 50%);
  transition: clip-path $duration $easing;
}
.switch__input:checked ~ .switch__track .switch__fill {
  clip-path: circle(75% at 82% 50%);
}

07Toggle icon mặt trời/mặt trăng

Bạn thấy gì

Chính track nơi núm di chuyển trở thành bầu trời: nền gradient dịch background-position từ nửa vàng sang nửa màu tối, còn vài chấm sao hiện dần lên qua opacity. Núm vọt quá chỗ rồi squash dẹt thành mặt trăng trước khi ổn định, còn bóng của track vốn đậm dưới ánh ngày sẽ mỏng dần khi màn đêm tràn tới.

Toggle icon mặt trời/mặt trăng
Mở trong tab mới

07 · Toggle icon mặt trời/mặt trăng — Diện tích đo được 1.343% · khung hình chuyển động 17/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

background-position별 opacity0.5초

Dùng ở đâu

Hợp với switch đổi theme hoặc cài đặt kiểu app thời tiết, nơi bản thân track đã kể hết câu chuyện ngày và đêm.

.switch__track {
  background-image: linear-gradient(90deg, $stage-yellow 0%, $stage-yellow 50%, $color 50%, $color 100%);
  background-size: 200% 100%;
  background-position: 0% 0%;
  transition: background-position $duration $easing;
}
.switch__input:checked ~ .switch__track { background-position: 100% 0%; }
.switch__input:checked ~ .switch__track .switch__star { opacity: 1; }

08Mixin biến thể kích cỡ

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

Một mixin SCSS nhận vào chiều rộng, chiều cao và kích cỡ núm rồi tự tính quãng đường --travel cần trượt, nên ba switch nhỏ/vừa/lớn ra đời từ đúng ba lời gọi @include, cả ba lại dùng chung một animation translateX nên đổi tốc độ ở một chỗ là cả ba cùng đổi theo. Độ vọt spring, tỉ lệ squash và biên độ đổi chiều sâu bóng cũng scale theo --travel, nên ba kích cỡ nào bấm vào cũng thấy đúng một cảm giác.

Mixin biến thể kích cỡ
Mở trong tab mới

08 · Mixin biến thể kích cỡ — Diện tích đo được 2.444% · khung hình chuyển động 16/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

@mixinem 단위0.25초

Chỗ dùng

Hợp với component trong design system, nơi không ai muốn đo lại quãng trượt mỗi lần thêm một kích cỡ mới.

@mixin switch-size($track-w, $track-h, $knob) {
  --travel: #{$track-w - $knob - 6px};
  .switch__track { width: $track-w; height: $track-h; }
  .switch__knob  { width: $knob; height: $knob; }
}
.switch--sm { @include switch-size(36px, 20px, 14px); }
.switch--md { @include switch-size(48px, 26px, 20px); }
.switch--lg { @include switch-size(60px, 32px, 26px); }

09Kiểu trạng thái disabled

Chuyển động thế nào

Switch mang thuộc tính disabled mờ dần qua filter: grayscale và opacity rồi đứng yên hẳn ở đó, con trỏ đổi thành not-allowed để xác nhận không còn bấm được nữa. Ngay cả nhịp mờ đi ấy vẫn còn một cú overshoot spring nhỏ và bóng mỏng dần theo, giữ nguyên chất liệu chung với tám switch kia cho tới khi đóng băng.

Kiểu trạng thái disabled
Mở trong tab mới

09 · Kiểu trạng thái disabled — Diện tích đo được 1.517% · 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

filter:grayscaleopacitycursor:not-allowed

Dùng khi nào

Hợp với mục cài đặt mà người dùng không có quyền, nơi trạng thái mờ đi tự nó đã là câu trả lời.

.switch__input:disabled ~ .switch__track { filter: grayscale(1); opacity: .45; }
.switch__input:disabled ~ .switch__label { opacity: .55; }

@keyframes fade-loop {
  0%, 20%  { filter: grayscale(0); opacity: 1; }
  50%, 70% { filter: grayscale(1); opacity: .45; }
  100%     { filter: grayscale(0); opacity: 1; }
}

Chỗ dễ hỏng ở đâu? — cái bẫy

Hai lớp chồng thay vì animate màu nền

Hơn nửa trong chín switch này không animate thẳng background-color mà xếp hai lớp chồng lên nhau rồi chỉ đổi opacity giữa chúng, như mục 01 và 04 đang làm. Nếu animate màu nền trực tiếp, giá trị giữa chừng phải nội suy qua không gian màu và mỗi trình duyệt trả về một sắc xám đục khác nhau, còn đổi độ trong suốt của hai lớp sẵn màu thì mỗi lớp vẫn giữ đúng màu gốc suốt quá trình. Tài liệu hiệu năng animation của MDN khuyên ưu tiên đúng hai thuộc tính transform và opacity vì lý do này. clip-path ở mục 06 và background-position ở mục 07 là hai ngoại lệ được giữ lại, vì cả hai chỉ buộc trình duyệt vẽ lại chứ không tính lại layout — caniuse.com xác nhận clip-path đã được các trình duyệt chính hỗ trợ từ rất lâu.

Bề rộng track và chữ ON OFF

Một điểm không hẳn là lỗi nhưng đáng nhớ: chữ ON và OFF ở mục 03 cần track rộng hơn hẳn đường kính núm, nếu không phần chữ sẽ bị che nửa vời ngay giữa cú trượt và trông rất vụng. Bản nén giữ nguyên các con số chiều rộng track và quãng trượt ghi ở trên chỉ mở ra khi gõ đúng mật khẩu a2ejkfw2, nút tải nằm ngay bên dưới.

Khả năng tiếp cận

Vai trò switch và focus bàn phím

Cả chín switch đều là <input type="checkbox"> gắn role="switch", nên Tab đưa focus tới và phím Space đổi giá trị như một checkbox thật (riêng 09 bị disabled nên bị loại khỏi thứ tự focus ngay từ đầu). :focus-visible được vẽ thêm một viền ngoài như bên dưới, và từ 01 đến 08, mỗi lần sự kiện change nổ ra thì aria-checked được JS set lại để trình đọc màn hình cũ không bỏ lỡ trạng thái, còn 09 vốn không bao giờ bật lại nên aria-checked="false" được gắn cố định ngay trong markup.

<label class="switch">
  <input type="checkbox" class="switch__input" role="switch" aria-checked="false">
  <span class="switch__track"><span class="switch__knob"></span></span>
</label>
.switch__input:focus-visible ~ .switch__track {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

Giảm chuyển động và trạng thái bật tắt

Khi prefers-reduced-motion: reduce được bật, cả chín switch chỉ tắt phần chuyển động còn trạng thái bật/tắt vẫn hiện đúng ngay lập tức. Riêng mục 09 vốn đã đứng yên vì bị khoá disabled, nên thiết lập này không đổi gì thêm ở đó. Tài liệu prefers-reduced-motion của MDN ghi chi tiết mức hỗ trợ trình duyệt cho media query này.

Muốn xem thêm control dạng checkbox khác thì ghé danh sách hiệu ứng CSS, còn muốn biết godrichstory thật ra bán gì thì đọc trang giới thiệu.

FAQ

Toggle switch khác checkbox thường ở chỗ nào?

Markup vẫn là cùng một <input type="checkbox">, chỉ thêm role="switch" để trình đọc màn hình gọi nó là switch thay vì checkbox. Giá trị đổi ngay khi bấm (như bật thông báo) hợp với switch hơn, còn giá trị chỉ có hiệu lực sau khi bấm nút lưu thì vẫn nên giữ checkbox.

Mục 09 đã disabled thì sao vẫn thấy chuyển động?

Bản demo trong lưới chỉ lặp lại hiệu ứng mờ dần để minh hoạ đúng lúc thuộc tính disabled bật lên, còn control thật luôn đứng yên ở trạng thái khoá. Không ai bấm tay vào bật tắt được nó cả.

Dùng trong React thì thay class bằng gì?

Component trong thư mục react/ của zip giữ checked bằng useState, còn ba biến số được nhận qua props rồi đẩy vào làm biến CSS. Module SCSS chỉ đọc var() nên đổi vài giá trị là ra một switch màu khác hoặc tốc độ khác ngay.

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.