Membuat komponen {i>switch

Ringkasan dasar tentang cara membangun komponen {i>switch<i} yang responsif dan dapat diakses.

Dalam postingan ini saya ingin berbagi pemikiran tentang cara membangun komponen {i>switch<i}. Coba demo.

Demo

Jika Anda lebih suka menonton video, berikut versi YouTube untuk postingan ini:

Ringkasan

Fungsi switch mirip dengan kotak centang tetapi secara eksplisit mewakili status aktif dan nonaktif boolean.

Demo ini menggunakan <input type="checkbox" role="switch"> untuk sebagian besar fungsionalitasnya, yang memiliki keuntungan karena tidak memerlukan CSS atau JavaScript untuk berfungsi penuh dan mudah diakses. Pemuatan CSS memberikan dukungan untuk file dari kanan-ke-kiri bahasa, verticalitas, animasi, dan banyak lagi. Pemuatan JavaScript beralih dapat ditarik dan berwujud.

Properti kustom

Variabel berikut mewakili berbagai bagian {i>switch<i} dan lainnya. Sebagai class tingkat atas, .gui-switch berisi properti khusus yang digunakan ke seluruh turunan komponen, dan titik entri untuk sistem dan penyesuaian.

Lacak

Panjang (--track-size), padding, dan dua warna:

.gui-switch {
  --track-size: calc(var(--thumb-size) * 2);
  --track-padding: 2px;

  --track-inactive: hsl(80 0% 80%);
  --track-active: hsl(80 60% 45%);

  --track-color-inactive: var(--track-inactive);
  --track-color-active: var(--track-active);

  @media (prefers-color-scheme: dark) {
    --track-inactive: hsl(80 0% 35%);
    --track-active: hsl(80 60% 60%);
  }
}

Kalimba

Ukuran, warna latar belakang, dan warna sorotan interaksi:

.gui-switch {
  --thumb-size: 2rem;
  --thumb: hsl(0 0% 100%);
  --thumb-highlight: hsl(0 0% 0% / 25%);

  --thumb-color: var(--thumb);
  --thumb-color-highlight: var(--thumb-highlight);

  @media (prefers-color-scheme: dark) {
    --thumb: hsl(0 0% 5%);
    --thumb-highlight: hsl(0 0% 100% / 25%);
  }
}

Pengurangan gerakan

Untuk menambahkan alias yang jelas dan mengurangi pengulangan, pengguna yang memiliki preferensi gerakan yang lebih rendah kueri media dapat dimasukkan ke properti kustom dengan antarmuka PostCSS plugin berdasarkan draf ini spesifikasi di Kueri Media 5:

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

Markup

Saya memilih untuk menggabungkan elemen <input type="checkbox" role="switch"> dengan <label>, memaketkan hubungannya untuk menghindari pengaitan kotak centang dan label ambiguitas, sekaligus memberikan pengguna kemampuan untuk berinteraksi dengan label aktifkan/nonaktifkan input.

J
label dan kotak centang natural, tanpa gaya.

<label for="switch" class="gui-switch">
  Label text
  <input type="checkbox" role="switch" id="switch">
</label>

<input type="checkbox"> telah dilengkapi dengan API dan state. Tujuan browser mengelola checked properti dan input peristiwa seperti oninputdan onchanged.

Tata letak

Flexbox, grid, dan custom properti sangat penting dalam mempertahankan gaya komponen ini. Mereka memusatkan nilai-nilai, memberikan nama untuk penghitungan atau area yang ambigu, dan memungkinkan properti kustom berukuran kecil API untuk penyesuaian komponen yang mudah.

.gui-switch

Tata letak tingkat atas untuk tombol ini adalah flexbox. Class .gui-switch berisi properti khusus pribadi dan publik yang digunakan anak-anak untuk menghitung tata letak.

Flexbox DevTools menempatkan tombol dan label horizontal, yang menunjukkan tata letaknya
distribusi ruang.

.gui-switch {
  display: flex;
  align-items: center;
  gap: 2ch;
  justify-content: space-between;
}

Memperluas dan memodifikasi tata letak flexbox seperti mengubah tata letak flexbox apa pun. Misalnya, untuk menempatkan label di atas atau di bawah tombol, atau untuk mengubah flex-direction:

Flexbox DevTools menempatkan label vertikal dan tombol.

<label for="light-switch" class="gui-switch" style="flex-direction: column">
  Default
  <input type="checkbox" role="switch" id="light-switch">
</label>

Lacak

Gaya input kotak centang ditata sebagai jalur peralihan dengan menghapus appearance: checkbox dan memberikan ukurannya sendiri:

Grid DevTools yang menempatkan jalur switch, menampilkan jalur grid yang diberi nama
area dengan nama &#39;track&#39;.

.gui-switch > input {
  appearance: none;

  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  padding: var(--track-padding);

  flex-shrink: 0;
  display: grid;
  align-items: center;
  grid: [track] 1fr / [track] 1fr;
}

Trek ini juga membuat area pelacakan kisi sel satu per satu untuk ibu jari klaim.

Kalimba

Gaya appearance: none juga menghapus tanda centang visual yang disediakan oleh browser. Komponen ini menggunakan sebuah elemen semu dan :checked pseudo-class pada input untuk ganti indikator visual ini.

Jempol adalah turunan elemen pseudo yang dilampirkan ke input[type="checkbox"] dan tumpukan di atas trek, bukan di bawahnya dengan mengklaim area grid track:

DevTools menampilkan thumb elemen semu seperti yang diposisikan di dalam petak CSS.

.gui-switch > input::before {
  content: "";
  grid-area: track;
  inline-size: var(--thumb-size);
  block-size: var(--thumb-size);
}

Gaya

Properti kustom memungkinkan komponen tombol akses serbaguna yang beradaptasi dengan warna skema, bahasa kanan-ke-kiri dan preferensi {i>motion<i}.

Perbandingan berdampingan antara tema terang dan gelap untuk tombol dan temanya
negara bagian.

Gaya interaksi sentuh

Di perangkat seluler, {i>browser<i} menambahkan sorotan ketuk dan fitur pemilihan teks ke label dan input. Ini berdampak negatif pada umpan balik gaya dan interaksi visual yang {i>switch<i} ini diperlukan. Dengan beberapa baris CSS saya bisa menghapus efek itu dan menambahkan gaya cursor: pointer saya sendiri:

.gui-switch {
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

Tidak selalu disarankan untuk menghapus gaya tersebut, karena dapat menjadi aset visual yang berharga masukan interaksi. Pastikan untuk memberikan alternatif khusus jika Anda menghapusnya.

Lacak

Gaya elemen ini sebagian besar tentang bentuk dan warnanya, yang diaksesnya dari induk .gui-switch melalui jenjang.

Varian tombol dengan ukuran dan warna trek kustom.

.gui-switch > input {
  appearance: none;
  border: none;
  outline-offset: 5px;
  box-sizing: content-box;

  padding: var(--track-padding);
  background: var(--track-color-inactive);
  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  border-radius: var(--track-size);
}

Berbagai macam pilihan penyesuaian untuk {i>switch track<i} berasal dari empat properti khusus. border: none ditambahkan karena appearance: none tidak hapus batas dari kotak centang di semua browser.

Kalimba

Elemen thumb sudah berada di track yang tepat, tetapi memerlukan gaya lingkaran:

.gui-switch > input::before {
  background: var(--thumb-color);
  border-radius: 50%;
}

DevTools ditampilkan untuk menandai elemen semu ibu jari lingkaran.

Interaksi

Menggunakan properti khusus untuk mempersiapkan interaksi yang akan menampilkan kursor sorotan dan perubahan posisi jempol. Pilihan pengguna juga diperiksa sebelum mentransisikan gaya sorotan {i>motion <i} atau pengarahan kursor.

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

Posisi jempol

Properti kustom menyediakan mekanisme sumber tunggal untuk memosisikan ibu jari di trek tersebut. Yang kami miliki adalah ukuran trek dan jempol yang akan kami gunakan di perhitungan untuk menjaga ibu jari tetap di-offset dengan benar dan di antara trek: 0% dan 100%.

Elemen input memiliki variabel posisi --thumb-position, dan ibu jari elemen pseudo menggunakannya sebagai posisi translateX:

.gui-switch > input {
  --thumb-position: 0%;
}

.gui-switch > input::before {
  transform: translateX(var(--thumb-position));
}

Sekarang kita bebas mengubah --thumb-position dari CSS dan pseudo-class yang tersedia pada elemen kotak centang. Karena kita secara kondisional menetapkan transition: transform var(--thumb-transition-duration) ease sebelumnya pada elemen ini, perubahan ini dapat beranimasi saat diubah:

/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
}

/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
}

Saya pikir orkestrasi yang terpisah ini berhasil dengan baik. Elemen thumb adalah hanya terkait dengan satu gaya, posisi translateX. Input dapat mengelola semua kompleksitas dan perhitungannya.

Vertikal

Dukungan dilakukan dengan class pengubah -vertical yang menambahkan rotasi dengan CSS bertransformasi ke elemen input.

Elemen yang diputar 3D tidak mengubah tinggi keseluruhan komponen, yang bisa menggagalkan tata letak blok. Akun untuk ini menggunakan --track-size dan Variabel --track-padding. Hitung jumlah ruang minimum yang diperlukan untuk tombol vertikal agar mengalir dalam tata letak seperti yang diharapkan:

.gui-switch.-vertical {
  min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));

  & > input {
    transform: rotate(-90deg);
  }
}

(RTL) kanan-ke-kiri

Seorang teman CSS, Elad Schecter, dan saya membuat prototipe bersama-sama geser menu samping menggunakan transformasi CSS yang ditangani dari kanan-ke-kiri bahasa dengan membalik satu variabel. Kita melakukannya karena tidak ada transformasi properti logis di CSS, dan mungkin tidak akan pernah ada. Elad memiliki ide cemerlang untuk menggunakan nilai properti kustom untuk membalik persentase, untuk memungkinkan pengelolaan lokasi tunggal dari logika untuk transformasi logis. Saya menggunakan teknik yang sama di {i>switch<i} ini dan saya berpikir itu berhasil dengan baik:

.gui-switch {
  --isLTR: 1;

  &:dir(rtl) {
    --isLTR: -1;
  }
}

Properti khusus yang disebut --isLTR awalnya memiliki nilai 1, yang berarti true karena tata letak dari kiri ke kanan secara default. Lalu, menggunakan CSS class semu :dir(), nilainya disetel ke -1 jika komponen berada dalam tata letak kanan-ke-kiri.

Terapkan --isLTR dengan menggunakannya dalam calc() di dalam transformasi:

.gui-switch.-vertical > input {
  transform: rotate(-90deg);
  transform: rotate(calc(90deg * var(--isLTR) * -1));
}

Sekarang, rotasi {i>switch<i} vertikal memperhitungkan posisi sisi yang berlawanan yang diperlukan oleh tata letak kanan-ke-kiri.

Transformasi translateX pada elemen semu thumb juga perlu diupdate ke memperhitungkan persyaratan sisi yang berlawanan:

.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
  --thumb-position: calc(
   ((var(--track-size) / 2) - (var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

Meskipun pendekatan ini tidak akan berhasil untuk menyelesaikan semua kebutuhan mengenai konsep seperti CSS yang logis bertransformasi, model ini memang menawarkan Prinsip DRY bagi banyak orang kasus penggunaan.

Negara bagian

Menggunakan input[type="checkbox"] bawaan tidak akan lengkap tanpa menangani berbagai status yang ada di dalamnya: :checked, :disabled, :indeterminate dan :hover. :focus sengaja dibiarkan sendirian, dengan penyesuaian hanya dilakukan pada offsetnya; lingkaran fokus tampak bagus di Firefox dan Safari:

Screenshot cincin fokus yang berfokus pada tombol di Firefox dan Safari.

Dicentang

<label for="switch-checked" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>

Status ini mewakili status on. Dalam status ini, input "{i>track<i}" latar belakang disetel ke warna aktif dan posisi ibu jari disetel ke " "akhir".

.gui-switch > input:checked {
  background: var(--track-color-active);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

Nonaktif

<label for="switch-disabled" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>

Tombol :disabled tidak hanya terlihat berbeda secara visual, tetapi juga harus membuat elemen immutable.Imutabilitas interaksi bebas dari browser, tetapi status visual memerlukan gaya karena penggunaan appearance: none.

.gui-switch > input:disabled {
  cursor: not-allowed;
  --thumb-color: transparent;

  &::before {
    cursor: not-allowed;
    box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);

    @media (prefers-color-scheme: dark) { & {
      box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
    }}
  }
}

Tombol bergaya gelap dinonaktifkan, dicentang, dan tidak dicentang
negara bagian.

Keadaan ini rumit karena memerlukan tema gelap dan terang dengan status yang dicentang. Saya memilih gaya minimal untuk status ini agar mudah beban pemeliharaan kombinasi gaya.

Tidak tentu

Status yang sering terlupakan adalah :indeterminate, dengan kotak centang tidak dicentang atau tidak dicentang. Kondisi ini menyenangkan, mengundang dan tidak terlalu terang-terangan. Bagus pengingat bahwa status boolean bisa tersembunyi di antara status.

Sangat sulit untuk menetapkan kotak centang ke angka yang tidak tentu, hanya JavaScript yang dapat menyetelnya:

<label for="switch-indeterminate" class="gui-switch">
  Indeterminate
  <input type="checkbox" role="switch" id="switch-indeterminate">
  <script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>

Keadaan tidak tentu yang 
memiliki ibu jari lagu pada
tengah, untuk menunjukkan 
keraguan.

Karena negara bagian, bagi saya, sederhana dan mengundang, rasanya tepat untuk menempatkan posisi tombol {i>switch<i} di tengah:

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

Arahkan kursor

Interaksi pengarahan kursor harus memberikan dukungan visual untuk UI yang terhubung dan juga memberikan arahan terhadap UI interaktif. Tombol ini menyoroti ibu jari dengan cincin semi-transparan saat label atau input diarahkan ke kursor. Pengarahan kursor ini kemudian memberikan arah ke elemen thumb interaktif.

"Sorotan" Efek dilakukan dengan box-shadow. Saat mengarahkan kursor, dari input yang tidak dinonaktifkan, tingkatkan ukuran --highlight-size. Jika pengguna tidak keberatan dengan gerakan, kita akan mentransisikan box-shadow dan melihatnya berkembang, jika mereka tidak cocok dengan gerakan, sorotan akan langsung muncul:

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

.gui-switch > input:not(:disabled):hover::before {
  --highlight-size: .5rem;
}

JavaScript

Bagi saya, antarmuka {i>switch<i} bisa terasa aneh saat mengemulasikan antarmuka khusus, terutama dengan lingkaran di dalam lintasan. iOS benar dengan {i>switch<i} mereka, Anda dapat menariknya dari sisi ke sisi, dan sangat memuaskan untuk memiliki opsi tersebut. Sebaliknya, elemen UI bisa terasa tidak aktif jika {i>drag gestur<i} sudah dicoba dan tidak terjadi apa-apa.

Jempol yang dapat ditarik

Elemen semu thumb menerima posisinya dari .gui-switch > input dengan cakupan var(--thumb-position), JavaScript dapat menyediakan nilai gaya inline pada input untuk memperbarui posisi ibu jari secara dinamis sehingga tampak mengikuti gestur pointer. Saat pointer dilepaskan, hapus gaya inline dan menentukan apakah penyeretan lebih dekat ke nonaktif atau aktif dengan menggunakan properti khusus --thumb-position. Ini adalah inti dari solusinya; peristiwa pointer melacak posisi pointer secara bersyarat untuk mengubah properti khusus CSS.

Karena komponen telah berfungsi 100% sebelum skrip ini ditampilkan dibutuhkan banyak upaya untuk mempertahankan perilaku yang ada, seperti mengeklik label untuk beralih input. JavaScript tidak boleh menambahkan fitur pada mengorbankan fitur yang ada.

touch-action

{i>Drag <i}(menyeret) adalah sebuah {i>gesture<i}, yang merupakan salah satu gestur yang tepat untuk Manfaat touch-action. Dalam kasus {i>switch<i} ini, {i>gesture <i}horizontal harus ditangani oleh skrip, atau gestur vertikal yang ditangkap untuk tombol vertikal yang berbeda. Dengan touch-action, kita dapat memberi tahu browser tentang gestur yang harus ditangani elemen ini, sehingga skrip dapat menangani {i>gesture <i}tanpa kompetisi.

CSS berikut memberi tahu browser bahwa saat gestur pointer dimulai dari dalam jalur peralihan ini, menangani gestur vertikal, tidak melakukan apa pun dengan satu:

.gui-switch > input {
  touch-action: pan-y;
}

Hasil yang diinginkan adalah gestur horizontal yang juga tidak menggeser atau men-scroll kami. Pointer bisa melakukan scroll secara vertikal mulai dari dalam input lalu men-scroll halaman, tetapi yang horizontal ditangani secara khusus.

Utilitas gaya nilai piksel

Saat penyiapan dan selama tarik, berbagai nilai angka yang dihitung perlu diambil dari elemen. Fungsi JavaScript berikut menampilkan nilai piksel yang dihitung diberi properti CSS. Ini digunakan dalam skrip pengaturan seperti ini getStyle(checkbox, 'padding-left').

​​const getStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}

const getPseudoStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}

export {
  getStyle,
  getPseudoStyle,
}

Perhatikan cara window.getComputedStyle() menerima argumen kedua, yaitu elemen pseudo target. Cukup rapi bahwa JavaScript dapat membaca begitu banyak nilai dari elemen, bahkan dari elemen semu.

dragging

Ini adalah momen inti untuk logika {i>drag<i} dan ada beberapa hal yang perlu diperhatikan dari pengendali peristiwa fungsi:

const dragging = event => {
  if (!state.activethumb) return

  let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
  let directionality = getStyle(state.activethumb, '--isLTR')

  let track = (directionality === -1)
    ? (state.activethumb.clientWidth * -1) + thumbsize + padding
    : 0

  let pos = Math.round(event.offsetX - thumbsize / 2)

  if (pos < bounds.lower) pos = 0
  if (pos > bounds.upper) pos = bounds.upper

  state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}

Banner besar skrip adalah state.activethumb, lingkaran kecil tempat skrip ini berada pemosisian bersama dengan pointer. Objek switches adalah Map() dengan adalah milik .gui-switch dan nilainya di-cache dalam batas dan ukuran yang menjaga dan membuat skrip secara efisien. Kanan-ke-kiri ditangani menggunakan properti khusus yang sama CSS tersebut adalah --isLTR, dan dapat menggunakannya untuk membalikkan logika dan melanjutkan mendukung RTL. event.offsetX juga berharga, karena berisi delta untuk memosisikan ibu jari.

state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)

Baris terakhir CSS ini menetapkan properti khusus yang digunakan oleh elemen jempol. Ini penetapan nilai akan bertransisi dari waktu ke waktu, tetapi petunjuk sebelumnya peristiwa telah menetapkan --thumb-transition-duration ke 0s untuk sementara, sehingga menghapus adalah interaksi yang lambat.

dragEnd

Agar pengguna diizinkan untuk menyeret jauh ke luar {i>switch<i} dan melepaskannya, sebuah peristiwa jendela global yang diperlukan terdaftar:

window.addEventListener('pointerup', event => {
  if (!state.activethumb) return

  dragEnd(event)
})

Menurut saya, sangat penting bagi pengguna untuk memiliki kebebasan untuk menarik dengan longgar dan memiliki antarmuka yang cukup cerdas untuk memperhitungkannya. Tidak perlu banyak waktu dengan peralihan ini, tetapi perlu pertimbangan yang cermat selama pengembangan {i>checkout<i}.

const dragEnd = event => {
  if (!state.activethumb) return

  state.activethumb.checked = determineChecked()

  if (state.activethumb.indeterminate)
    state.activethumb.indeterminate = false

  state.activethumb.style.removeProperty('--thumb-transition-duration')
  state.activethumb.style.removeProperty('--thumb-position')
  state.activethumb.removeEventListener('pointermove', dragging)
  state.activethumb = null

  padRelease()
}

Interaksi dengan elemen telah selesai, waktu untuk menyetel input telah diperiksa dan menghapus semua peristiwa gestur. Kotak centang diubah dengan state.activethumb.checked = determineChecked().

determineChecked()

Fungsi ini, yang dipanggil oleh dragEnd, menentukan lokasi thumb saat ini dalam batas jalurnya dan mengembalikan nilai benar jika sama dengan atau lebih setengah dari lintasan:

const determineChecked = () => {
  let {bounds} = switches.get(state.activethumb.parentElement)

  let curpos =
    Math.abs(
      parseInt(
        state.activethumb.style.getPropertyValue('--thumb-position')))

  if (!curpos) {
    curpos = state.activethumb.checked
      ? bounds.lower
      : bounds.upper
  }

  return curpos >= bounds.middle
}

Masukan tambahan

Gerakan {i>drag<i} menimbulkan sedikit biaya pada kode karena pada struktur HTML awal dipilih, terutama menggabungkan input dalam label. Label, menjadi orang tua akan menerima interaksi klik setelah input. Pada akhirnya, Peristiwa dragEnd, Anda mungkin menyadari padRelease() sebagai kata-kata yang aneh .

const padRelease = () => {
  state.recentlyDragged = true

  setTimeout(_ => {
    state.recentlyDragged = false
  }, 300)
}

Hal ini untuk memperhitungkan label yang mendapatkan klik nanti, karena akan menghapus centang, atau memeriksa interaksi yang dilakukan pengguna.

Jika harus melakukannya lagi, saya mungkin mempertimbangkan untuk menyesuaikan DOM dengan JavaScript selama upgrade UX, untuk membuat elemen yang menangani klik label itu sendiri dan tidak bertentangan dengan perilaku bawaan.

Jenis JavaScript ini paling tidak saya sukai untuk ditulis, saya tidak ingin mengelola gelembung kejadian bersyarat:

const preventBubbles = event => {
  if (state.recentlyDragged)
    event.preventDefault() && event.stopPropagation()
}

Kesimpulan

Komponen {i>switch<i} kecil ini akhirnya menjadi pekerjaan yang paling banyak dari semua Tantangan GUI sejauh ini! Sekarang setelah Anda tahu bagaimana saya melakukannya, bagaimana Anda akan 🙂

Mari kita diversifikasi pendekatan kami dan mempelajari semua cara untuk membangun di web. Buat demo, link tweet saya, dan saya akan menambahkannya ke bagian remix komunitas di bawah ini.

Remix komunitas

Resource

Temukan kode sumber .gui-switch di GitHub.