Xây dựng thành phần chuyển đổi giao diện

Thông tin tổng quan cơ bản về cách tạo thành phần chuyển đổi giao diện thích ứng và dễ tiếp cận.

Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về cách tạo thành phần chuyển đổi giao diện tối và sáng. Dùng thử bản minh hoạ.

Bản minh hoạ kích thước của nút đã tăng để dễ dàng hiển thị

Nếu bạn thích video, đây là phiên bản YouTube của bài đăng này:

Tổng quan

Một trang web có thể cung cấp các chế độ cài đặt để kiểm soát bảng phối màu thay vì hoàn toàn dựa vào lựa chọn ưu tiên của hệ thống. Điều này có nghĩa là người dùng có thể duyệt web ở một chế độ khác với lựa chọn ưu tiên của hệ thống. Ví dụ: hệ thống của người dùng ở giao diện sáng, nhưng người dùng muốn trang web hiển thị trong giao diện tối.

Có một số cân nhắc về kỹ thuật web khi xây dựng tính năng này. Ví dụ: trình duyệt phải nhận biết được lựa chọn ưu tiên này càng sớm càng tốt để ngăn màu trang nhấp nháy. Trước tiên, chế độ kiểm soát cần đồng bộ hoá với hệ thống, sau đó cho phép các ngoại lệ được lưu trữ phía máy khách.

Sơ đồ này cho thấy bản xem trước sự kiện tương tác trên tài liệu và lượt tải trang JavaScript để thấy tổng thể có 4 đường dẫn để đặt giao diện

Markup (note: đây là tên ứng dụng)

Bạn nên sử dụng <button> cho nút bật/tắt, vì sau đó, bạn sẽ được hưởng lợi từ các sự kiện và tính năng tương tác do trình duyệt cung cấp, chẳng hạn như sự kiện nhấp chuột và khả năng tập trung.

Nút

Nút này cần có lớp để sử dụng từ CSS và mã nhận dạng để sử dụng từ JavaScript. Ngoài ra, vì nội dung của nút là biểu tượng chứ không phải văn bản, hãy thêm thuộc tính title (tiêu đề) để cung cấp thông tin về mục đích của nút. Cuối cùng, hãy thêm [aria-label] để giữ trạng thái của nút biểu tượng để trình đọc màn hình có thể chia sẻ trạng thái của giao diện với người khiếm thị.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto"
>
  …
</button>

aria-labelaria-live lịch sự

Để cho trình đọc màn hình biết rằng các thay đổi đối với aria-label cần được thông báo, hãy thêm aria-live="polite" vào nút này.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto" 
  aria-live="polite"
>
  …
</button>

Việc bổ sung mã đánh dấu này sẽ báo hiệu cho trình đọc màn hình một cách lịch sự thay vì aria-live="assertive", hãy cho người dùng biết nội dung đã thay đổi. Trong trường hợp của nút này, nút này sẽ thông báo "sáng" hoặc "tối" tuỳ thuộc vào tính năng của aria-label.

Biểu tượng đồ hoạ vectơ có thể mở rộng (SVG)

SVG cung cấp một cách để tạo hình dạng chất lượng cao, có thể mở rộng mà chỉ cần đánh dấu tối thiểu. Việc tương tác với nút này có thể kích hoạt trạng thái trực quan mới cho các vectơ, giúp SVG trở nên phù hợp với các biểu tượng.

Mã đánh dấu SVG sau đây nằm bên trong <button>:

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  …
</svg>

aria-hidden đã được thêm vào phần tử SVG để trình đọc màn hình biết và bỏ qua phần tử này vì phần tử này được đánh dấu là đang trình bày. Bạn nên sử dụng tính năng này để trang trí cho hình ảnh, chẳng hạn như biểu tượng bên trong nút. Ngoài thuộc tính viewBox bắt buộc trên phần tử, hãy thêm chiều cao và chiều rộng vì các lý do tương tự để hình ảnh cần có kích thước cùng dòng.

Mặt trời

Biểu tượng mặt trời hiển thị với các tia nắng đã mờ dần và một mũi tên màu hồng đậm
  chỉ vào vòng tròn ở giữa.

Đồ hoạ mặt trời bao gồm một vòng tròn và các đường kẻ mà SVG có hình dạng một cách thuận tiện. <circle> được căn giữa bằng cách đặt các thuộc tính cxcy thành 12, bằng một nửa kích thước khung nhìn (24), sau đó cung cấp bán kính (r) là 6 giúp đặt kích thước.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>

Ngoài ra, thuộc tính mặt nạ trỏ đến một mã nhận dạng của phần tử SVG mà bạn sẽ tạo tiếp theo và cuối cùng chọn màu nền khớp với màu văn bản của trang bằng currentColor.

Tia nắng

Biểu tượng mặt trời hiển thị với tâm điểm mặt trời đã mờ đi và một mũi tên màu hồng đậm
  chỉ vào tia nắng.

Tiếp theo, các đường tia nắng được thêm ngay bên dưới vòng tròn, bên trong nhóm <g> của phần tử nhóm.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    <line x1="12" y1="1" x2="12" y2="3" />
    <line x1="12" y1="21" x2="12" y2="23" />
    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
    <line x1="1" y1="12" x2="3" y2="12" />
    <line x1="21" y1="12" x2="23" y2="12" />
    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
  </g>
</svg>

Lần này, thay vì giá trị điềncurrentColor, nét của mỗi dòng sẽ được đặt. Các đường kẻ cùng với hình tròn tạo nên mặt trời đẹp mắt với những tia sáng.

Mặt trăng

Để tạo ảo giác về sự chuyển đổi liền mạch giữa ánh sáng (mặt trời) và bóng tối (mặt trăng), mặt trăng là sự bổ sung của biểu tượng mặt trời, sử dụng mặt nạ SVG.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    …
  </g>
  <mask class="moon" id="moon-mask">
    <rect x="0" y="0" width="100%" height="100%" fill="white" />
    <circle cx="24" cy="10" r="6" fill="black" />
  </mask>
</svg>
Đồ hoạ với 3 lớp dọc giúp thể hiện cách hoạt động của tính năng tạo mặt nạ. Lớp trên cùng là một hình vuông màu trắng có một vòng tròn màu đen. Lớp ở giữa là biểu tượng mặt trời.
Lớp dưới cùng được gắn nhãn là kết quả và hiển thị biểu tượng mặt trời với một đường cắt nơi có vòng tròn màu đen của lớp trên cùng.

Mặt nạ với SVG (Đồ hoạ vectơ có thể mở rộng) rất hiệu quả, cho phép màu trắng và đen có thể loại bỏ hoặc bao gồm một phần của một đồ hoạ khác. Biểu tượng mặt trời sẽ bị hình mặt trăng <circle> làm che khuất bởi một mặt nạ SVG, chỉ cần di chuyển một hình tròn vào và ra khỏi một vùng mặt nạ.

Điều gì sẽ xảy ra nếu CSS không tải?

Ảnh chụp màn hình một nút trình duyệt thuần tuý có biểu tượng mặt trời bên trong.

Bạn nên kiểm thử SVG như thể CSS không tải để đảm bảo kết quả không quá lớn hoặc gây ra vấn đề về bố cục. Các thuộc tính chiều cao và chiều rộng cùng dòng trên SVG cộng với việc sử dụng currentColor cung cấp các quy tắc kiểu tối thiểu để trình duyệt sử dụng nếu CSS không tải. Điều này mang lại phong cách phòng thủ đẹp mắt chống lại sự bất ổn của mạng.

Bố cục

Thành phần chuyển đổi giao diện có ít diện tích bề mặt nên bạn không cần bố cục lưới hoặc hộp linh hoạt. Thay vào đó, phương thức định vị SVG và phép biến đổi CSS sẽ được sử dụng.

Kiểu

.theme-toggle kiểu

Phần tử <button> là vùng chứa cho các hình dạng và kiểu của biểu tượng. Ngữ cảnh gốc này sẽ giữ lại các màu và kích thước thích ứng để chuyển xuống SVG.

Nhiệm vụ đầu tiên là đặt nút thành một vòng tròn và xoá các kiểu nút mặc định:

.theme-toggle {
  --size: 2rem;
  
  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
}

Tiếp theo, hãy thêm một số kiểu tương tác. Thêm kiểu con trỏ cho người dùng chuột. Thêm touch-action: manipulation để mang lại trải nghiệm chạm phản ứng nhanh. Xoá phần đánh dấu bán trong suốt mà iOS áp dụng cho các nút. Cuối cùng, hãy để trạng thái lấy nét phác thảo một số khoảng trống từ cạnh của phần tử:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;
}

SVG bên trong nút cũng cần có một số kiểu. SVG phải vừa với kích thước của nút và để tạo sự mềm mại về mặt hình ảnh, hãy bo tròn các đầu đường kẻ:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;

  & > svg {
    inline-size: 100%;
    block-size: 100%;
    stroke-linecap: round;
  }
}

Điều chỉnh kích thước thích ứng bằng truy vấn nội dung nghe nhìn hover

Kích thước nút biểu tượng hơi nhỏ tại 2rem, phù hợp với người dùng chuột nhưng có thể gây khó khăn cho con trỏ thô như ngón tay. Làm cho nút đáp ứng nhiều nguyên tắc về kích thước cảm ứng bằng cách sử dụng truy vấn nội dung nghe nhìn di chuột để chỉ định mức tăng kích thước.

.theme-toggle {
  --size: 2rem;
  …
  
  @media (hover: none) {
    --size: 48px;
  }
}

Kiểu SVG mặt trời và mặt trăng

Nút này chứa các khía cạnh tương tác của thành phần chuyển đổi giao diện, còn SVG bên trong sẽ chứa các khía cạnh hình ảnh và ảnh động. Đây là nơi biểu tượng có thể được làm đẹp và sống động.

Giao diện sáng

ALT_TEXT_HERE

Để điều chỉnh tỷ lệ và xoay ảnh động xảy ra từ tâm của hình dạng SVG, hãy đặt transform-origin: center center của chúng. Các màu thích ứng do nút cung cấp được sử dụng ở đây bởi các hình dạng. Mặt trăng và mặt trời sử dụng nút được cung cấp var(--icon-fill)var(--icon-fill-hover) để tô màu nền, còn tia nắng sử dụng các biến cho nét vẽ.

.sun-and-moon {
  & > :is(.moon, .sun, .sun-beams) {
    transform-origin: center center;
  }

  & > :is(.moon, .sun) {
    fill: var(--icon-fill);

    @nest .theme-toggle:is(:hover, :focus-visible) > & {
      fill: var(--icon-fill-hover);
    }
  }

  & > .sun-beams {
    stroke: var(--icon-fill);
    stroke-width: 2px;

    @nest .theme-toggle:is(:hover, :focus-visible) & {
      stroke: var(--icon-fill-hover);
    }
  }
}

Giao diện tối

ALT_TEXT_HERE

Kiểu mặt trăng cần phải xoá các tia nắng, mở rộng vòng tròn mặt trời và di chuyển mặt nạ hình tròn.

.sun-and-moon {
  @nest [data-theme="dark"] & {
    & > .sun {
      transform: scale(1.75);
    }

    & > .sun-beams {
      opacity: 0;
    }

    & > .moon > circle {
      transform: translateX(-7px);

      @supports (cx: 1) {
        transform: translateX(0);
        cx: 17;
      }
    }
  }
}

Lưu ý rằng giao diện tối không có thay đổi hoặc chuyển đổi màu sắc. Thành phần nút mẹ sở hữu màu sắc mà chúng vốn thích ứng trong bối cảnh tối và sáng. Thông tin chuyển đổi phải đằng sau truy vấn phương tiện lựa chọn ưu tiên chuyển động của người dùng.

Hoạt ảnh

Nút phải hoạt động và có trạng thái nhưng không có chuyển đổi tại thời điểm này. Các phần sau đây trình bày toàn bộ nội dung về việc xác định quá trình chuyển đổi cách thứccái gì.

Chia sẻ truy vấn đa phương tiện và nhập gia tốc

Để giúp dễ dàng đặt hiệu ứng chuyển đổi và ảnh động phía sau lựa chọn ưu tiên về chuyển động của người dùng trên hệ điều hành, trình bổ trợ PostCSS Custom Media cho phép sử dụng cú pháp thông số kỹ thuật CSS nháp cho các biến truy vấn nội dung nghe nhìn:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

/* usage example */
@media (--motionOK) {
  .sun {
    transition: transform .5s var(--ease-elastic-3);
  }
}

Để có các gia tốc CSS độc đáo và dễ sử dụng, hãy nhập phần gia tốc của Open Props:

@import "https://unpkg.com/open-props/easings.min.css";

/* usage example */
.sun {
  transition: transform .5s var(--ease-elastic-3);
}

Mặt trời

Quá trình chuyển đổi của mặt trời sẽ vui tươi hơn mặt trăng, nhờ đó đạt được hiệu ứng này nhờ những bước nhảy vọt. Các tia nắng sẽ nảy một lượng nhỏ khi chúng xoay và tâm mặt trời sẽ nảy một lượng nhỏ khi mở rộng.

Kiểu mặc định (giao diện sáng) xác định hiệu ứng chuyển đổi, còn kiểu giao diện tối xác định các tuỳ chỉnh để chuyển đổi sang giao diện sáng:

​​.sun-and-moon {
  @media (--motionOK) {
    & > .sun {
      transition: transform .5s var(--ease-elastic-3);
    }

    & > .sun-beams {
      transition: 
        transform .5s var(--ease-elastic-4),
        opacity .5s var(--ease-3)
      ;
    }

    @nest [data-theme="dark"] & {
      & > .sun {
        transform: scale(1.75);
        transition-timing-function: var(--ease-3);
        transition-duration: .25s;
      }

      & > .sun-beams {
        transform: rotateZ(-25deg);
        transition-duration: .15s;
      }
    }
  }
}

Trong bảng điều khiển Ảnh động trong Công cụ của Chrome cho nhà phát triển, bạn có thể tìm thấy tiến trình chuyển đổi ảnh động. Bạn có thể kiểm tra thời lượng của tổng ảnh động, các thành phần và thời gian gia tốc.

Chuyển đổi sáng sang tối
Chuyển đổi từ tối sang sáng

Mặt trăng

Các vị trí ánh sáng và tối của mặt trăng đã được đặt sẵn, hãy thêm các kiểu chuyển đổi vào bên trong truy vấn nội dung nghe nhìn --motionOK để hiện thực hoá truy vấn đó trong khi vẫn tôn trọng các lựa chọn ưu tiên về chuyển động của người dùng.

Thời gian kèm theo độ trễ và thời lượng là yếu tố rất quan trọng để quá trình chuyển đổi này diễn ra suôn sẻ. Ví dụ: nếu mặt trời bị che khuất quá sớm, quá trình chuyển đổi sẽ tạo cảm giác không được sắp xếp hoặc vui tươi, sẽ hỗn độn.

​​.sun-and-moon {
  @media (--motionOK) {
    & .moon > circle {
      transform: translateX(-7px);
      transition: transform .25s var(--ease-out-5);

      @supports (cx: 1) {
        transform: translateX(0);
        cx: 17;
        transition: cx .25s var(--ease-out-5);
      }
    }

    @nest [data-theme="dark"] & {
      & > .moon > circle {
        transition-delay: .25s;
        transition-duration: .5s;
      }
    }
  }
}
Chuyển đổi sáng sang tối
Chuyển đổi từ tối sang sáng

Ưu tiên giảm chuyển động

Trong hầu hết các Thử thách GUI, tôi cố gắng giữ một số ảnh động, chẳng hạn như làm mờ dần độ mờ, cho người dùng thích giảm chuyển động. Tuy nhiên, thành phần này cho trải nghiệm tốt hơn khi được thay đổi trạng thái tức thì.

JavaScript

Bạn cần làm rất nhiều việc cho JavaScript trong thành phần này, từ việc quản lý thông tin ARIA cho trình đọc màn hình đến việc lấy và đặt giá trị từ bộ nhớ cục bộ.

Trải nghiệm tải trang

Điều quan trọng là không có hiện tượng nhấp nháy màu xảy ra khi tải trang. Nếu người dùng có bảng phối màu tối cho biết họ thích sáng với thành phần này, thì hãy tải lại trang, lúc đầu trang sẽ tối rồi chuyển sang sáng. Để tránh điều này, bạn cần chạy một phần nhỏ chặn JavaScript với mục tiêu đặt thuộc tính HTML data-theme càng sớm càng tốt.

<script src="./theme-toggle.js"></script>

Để làm việc này, trước tiên, bạn cần tải một thẻ <script> thuần tuý trong tài liệu <head> trước mọi mã đánh dấu CSS hoặc <body>. Khi gặp một tập lệnh chưa được đánh dấu như thế này, trình duyệt sẽ chạy mã và thực thi mã đó trước phần còn lại của HTML. Khi sử dụng khoảnh khắc chặn này một cách thận trọng, bạn có thể đặt thuộc tính HTML trước khi CSS chính vẽ trang, nhờ đó tránh được hiện tượng đèn flash hoặc màu sắc.

Trước tiên, JavaScript sẽ kiểm tra lựa chọn ưu tiên của người dùng trong bộ nhớ cục bộ và tính năng dự phòng để kiểm tra lựa chọn ưu tiên của hệ thống nếu không tìm thấy gì trong bộ nhớ:

const storageKey = 'theme-preference'

const getColorPreference = () => {
  if (localStorage.getItem(storageKey))
    return localStorage.getItem(storageKey)
  else
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light'
}

Tiếp theo, một hàm để thiết lập lựa chọn ưu tiên của người dùng trong bộ nhớ cục bộ sẽ được phân tích cú pháp:

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

Tiếp theo là một hàm để sửa đổi tài liệu bằng các lựa chọn ưu tiên.

const reflectPreference = () => {
  document.firstElementChild
    .setAttribute('data-theme', theme.value)

  document
    .querySelector('#theme-toggle')
    ?.setAttribute('aria-label', theme.value)
}

Điều quan trọng cần lưu ý tại thời điểm này là trạng thái phân tích cú pháp tài liệu HTML. Trình duyệt chưa biết về nút "#theme-toggle" vì thẻ <head> chưa được phân tích cú pháp hoàn chỉnh. Tuy nhiên, trình duyệt có một document.firstElementChild, còn gọi là thẻ <html>. Hàm này cố gắng đặt cả hai để tiếp tục đồng bộ hoá, nhưng trong lần chạy đầu tiên, bạn chỉ có thể đặt thẻ HTML. Lúc đầu, querySelector sẽ không tìm thấy dữ liệu nào và toán tử tạo chuỗi không bắt buộc sẽ đảm bảo không có lỗi cú pháp khi không tìm thấy và hệ thống sẽ cố gắng gọi hàm setAttribute.

Tiếp theo, hàm reflectPreference() đó được gọi ngay lập tức để tài liệu HTML có tập hợp thuộc tính data-theme:

reflectPreference()

Nút này vẫn cần thuộc tính, vì vậy, hãy đợi sự kiện tải trang, sau đó thì việc truy vấn, thêm trình nghe và đặt thuộc tính trên sẽ an toàn:

window.onload = () => {
  // set on load so screen readers can get the latest value on the button
  reflectPreference()

  // now this script can find and listen for clicks on the control
  document
    .querySelector('#theme-toggle')
    .addEventListener('click', onClick)
}

Trải nghiệm bật/tắt

Khi người dùng nhấp vào nút này, giao diện cần được hoán đổi, trong bộ nhớ JavaScript và trong tài liệu. Bạn sẽ cần kiểm tra giá trị giao diện hiện tại và đưa ra quyết định về trạng thái mới của giao diện. Sau khi đặt trạng thái mới, hãy lưu và cập nhật tài liệu:

const onClick = () => {
  theme.value = theme.value === 'light'
    ? 'dark'
    : 'light'

  setPreference()
}

Đang đồng bộ hoá với hệ thống

Điểm duy nhất của nút chuyển giao diện này là đồng bộ hoá với lựa chọn ưu tiên của hệ thống khi tuỳ chọn này thay đổi. Nếu người dùng thay đổi lựa chọn ưu tiên của hệ thống trong khi một trang và thành phần này đang hiển thị, thì nút chuyển giao diện sẽ thay đổi để phù hợp với lựa chọn ưu tiên của người dùng mới, như thể người dùng tương tác với nút chuyển giao diện cùng lúc với việc chuyển đổi hệ thống.

Thực hiện điều này bằng JavaScript và một sự kiện matchMedia theo dõi các thay đổi đối với truy vấn nội dung nghe nhìn:

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
Việc thay đổi lựa chọn ưu tiên của hệ thống MacOS sẽ làm thay đổi trạng thái chuyển đổi của giao diện

Kết luận

Giờ bạn đã biết tôi làm được như thế nào, bạn sẽ làm thế nào 🙂

Hãy đa dạng hoá phương pháp tiếp cận của chúng ta và tìm hiểu tất cả các cách xây dựng trên web. Hãy tạo một bản minh hoạ, đường liên kết tweet me và tôi sẽ thêm bản phối lại đó vào phần bản phối lại của cộng đồng bên dưới!

Bản phối lại của cộng đồng