Membuat komponen tombol terpisah

Ringkasan dasar tentang cara mem-build komponen tombol terpisah yang mudah diakses.

Dalam postingan ini, saya ingin membagikan pemikiran tentang cara membuat tombol terpisah . Coba demo.

Demo

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

Ringkasan

Tombol terpisah adalah tombol yang menyembunyikan tombol utama dan daftar tombol tambahan. Fungsi ini berguna untuk mengekspos tindakan umum sekaligus menyusun tindakan sekunder yang kurang sering digunakan hingga diperlukan. Tombol terpisah dapat sangat penting untuk membantu desain yang ramai terasa minimal. Tombol pemisahan lanjutan bahkan dapat mengingat tindakan pengguna terakhir dan mempromosikannya ke posisi utama.

Tombol pemisahan umum dapat ditemukan di aplikasi email Anda. Tindakan utama adalah kirim, tetapi Anda mungkin dapat mengirim nanti atau menyimpan draf:

Contoh tombol terpisah seperti yang terlihat di aplikasi email.

Area tindakan bersama sangat bagus, karena pengguna tidak perlu melihat-lihat. Mereka mengetahui bahwa tindakan email penting terdapat di tombol terpisah.

Suku cadang

Mari kita uraikan bagian-bagian penting dari tombol terpisah sebelum membahas orkestrasi secara keseluruhan dan pengalaman pengguna akhir. Alat inspeksi aksesibilitas VisBug digunakan di sini untuk membantu menampilkan tampilan makro komponen, yang menampilkan aspek HTML, gaya, dan aksesibilitas untuk setiap bagian utama.

Elemen HTML yang membentuk tombol terpisah.

Penampung tombol terpisah tingkat atas

Komponen tingkat tertinggi adalah flexbox inline, dengan class gui-split-button, yang berisi tindakan utama dan .gui-popup-button.

Class gui-split-button diperiksa dan menampilkan properti CSS yang digunakan di class ini.

Tombol tindakan utama

<button> yang awalnya terlihat dan dapat difokuskan sesuai dengan penampung dengan dua bentuk sudut yang cocok untuk interaksi fokus, arahkan kursor, dan aktif agar muncul dalam .gui-split-button.

Inspector menampilkan aturan CSS untuk elemen tombol.

Tombol pilihan pop-up

Elemen dukungan "tombol pop-up" digunakan untuk mengaktifkan dan merujuk ke daftar tombol sekunder. Perhatikan bahwa ini bukan <button> dan tidak dapat difokuskan. Namun, ini adalah anchor pemosisian untuk .gui-popup dan host untuk :focus-within yang digunakan untuk menampilkan pop-up.

Inspector menampilkan aturan CSS untuk class gui-popup-button.

Kartu pop-up

Ini adalah turunan kartu mengambang ke anchor .gui-popup-button, yang diposisikan secara absolut dan menggabungkan daftar tombol secara semantik.

Inspector yang menampilkan aturan CSS untuk class gui-popup

Tindakan sekunder

<button> yang dapat difokuskan dengan ukuran font yang sedikit lebih kecil daripada tombol tindakan utama menampilkan ikon dan gaya tambahan ke tombol utama.

Inspector menampilkan aturan CSS untuk elemen tombol.

Properti kustom

Variabel berikut membantu menciptakan harmoni warna dan tempat sentral untuk mengubah nilai yang digunakan di seluruh komponen.

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);

.gui-split-button {
  --theme:             hsl(220 75% 50%);
  --theme-hover:  hsl(220 75% 45%);
  --theme-active:  hsl(220 75% 40%);
  --theme-text:      hsl(220 75% 25%);
  --theme-border: hsl(220 50% 75%);
  --ontheme:         hsl(220 90% 98%);
  --popupbg:         hsl(220 0% 100%);

  --border: 1px solid var(--theme-border);
  --radius: 6px;
  --in-speed: 50ms;
  --out-speed: 300ms;

  @media (--dark) {
    --theme:             hsl(220 50% 60%);
    --theme-hover:  hsl(220 50% 65%);
    --theme-active:  hsl(220 75% 70%);
    --theme-text:      hsl(220 10% 85%);
    --theme-border: hsl(220 20% 70%);
    --ontheme:         hsl(220 90% 5%);
    --popupbg:         hsl(220 10% 30%);
  }
}

Tata letak dan warna

Markup

Elemen dimulai sebagai <div> dengan nama class kustom.

<div class="gui-split-button"></div>

Tambahkan tombol utama dan elemen .gui-popup-button.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions"></span>
</div>

Perhatikan atribut aria aria-haspopup dan aria-expanded. Isyarat ini sangat penting bagi pembaca layar untuk mengetahui kemampuan dan status pengalaman tombol terpisah. Atribut title bermanfaat bagi semua orang.

Tambahkan ikon <svg> dan elemen penampung .gui-popup.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup"></ul>
  </span>
</div>

Untuk penempatan pop-up yang mudah, .gui-popup adalah turunan dari tombol yang memperluasnya. Satu-satunya kendala dengan strategi ini adalah penampung .gui-split-button tidak dapat menggunakan overflow: hidden, karena akan memotong popup agar tidak terlihat secara visual.

<ul> yang diisi dengan konten <li><button> akan mengumumkan dirinya sebagai "daftar tombol" kepada pembaca layar, yang merupakan antarmuka yang ditampilkan.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li>
        <button>Schedule for later</button>
      </li>
      <li>
        <button>Delete</button>
      </li>
      <li>
        <button>Save draft</button>
      </li>
    </ul>
  </span>
</div>

Untuk memberikan nuansa dan bersenang-senang dengan warna, saya telah menambahkan ikon ke tombol sekunder dari https://heroicons.com. Ikon bersifat opsional untuk tombol utama dan sekunder.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
        </svg>
        Schedule for later
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
        </svg>
        Delete
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
        </svg>
        Save draft
      </button></li>
    </ul>
  </span>
</div>

Gaya

Dengan HTML dan konten yang sudah ada, gaya siap memberikan warna dan tata letak.

Menata gaya penampung tombol terpisah

Jenis tampilan inline-flex berfungsi dengan baik untuk komponen penggabungan ini karena harus sesuai dengan tombol, tindakan, atau elemen terpisah lainnya.

.gui-split-button {
  display: inline-flex;
  border-radius: var(--radius);
  background: var(--theme);
  color: var(--ontheme);
  fill: var(--ontheme);

  touch-action: manipulation;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

Tombol pemisahan.

Gaya visual <button>

Tombol sangat bagus dalam menyamarkan jumlah kode yang diperlukan. Anda mungkin perlu mengurungkan atau mengganti gaya default browser, tetapi Anda juga perlu menerapkan beberapa pewarisan, menambahkan status interaksi, dan beradaptasi dengan berbagai preferensi pengguna dan jenis input. Gaya tombol bertambah dengan cepat.

Tombol ini berbeda dengan tombol biasa karena memiliki latar belakang yang sama dengan elemen induk. Biasanya, tombol memiliki latar belakang dan warna teksnya sendiri. Namun, keduanya berbagi, dan hanya menerapkan latar belakangnya sendiri pada interaksi.

.gui-split-button button {
  cursor: pointer;
  appearance: none;
  background: none;
  border: none;

  display: inline-flex;
  align-items: center;
  gap: 1ch;
  white-space: nowrap;

  font-family: inherit;
  font-size: inherit;
  font-weight: 500;

  padding-block: 1.25ch;
  padding-inline: 2.5ch;

  color: var(--ontheme);
  outline-color: var(--theme);
  outline-offset: -5px;
}

Tambahkan status interaksi dengan beberapa pseudo-class CSS dan penggunaan properti kustom yang cocok untuk status:

.gui-split-button button {
  

  &:is(:hover, :focus-visible) {
    background: var(--theme-hover);
    color: var(--ontheme);

    & > svg {
      stroke: currentColor;
      fill: none;
    }
  }

  &:active {
    background: var(--theme-active);
  }
}

Tombol utama memerlukan beberapa gaya khusus untuk menyelesaikan efek desain:

.gui-split-button > button {
  border-end-start-radius: var(--radius);
  border-start-start-radius: var(--radius);

  & > svg {
    fill: none;
    stroke: var(--ontheme);
  }
}

Terakhir, untuk beberapa tampilan, tombol dan ikon tema terang mendapatkan bayangan:

.gui-split-button {
  @media (--light) {
    & > button,
    & button:is(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--theme-active);
    }
    & > .gui-popup-button > svg,
    & button:is(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--theme-active));
    }
  }
}

Tombol yang bagus telah memperhatikan interaksi mikro dan detail kecil.

Catatan tentang :focus-visible

Perhatikan bagaimana gaya tombol menggunakan :focus-visible, bukan :focus. :focus adalah sentuhan penting untuk membuat antarmuka pengguna yang dapat diakses, tetapi memiliki satu kelemahan: tidak cerdas dalam menentukan apakah pengguna perlu melihatnya atau tidak, fokus akan diterapkan untuk semua fokus.

Video di bawah mencoba menguraikan interaksi mikro ini, untuk menunjukkan bagaimana :focus-visible adalah alternatif yang cerdas.

Menata gaya tombol pop-up

Flexbox 4ch untuk memusatkan ikon dan mengaitkan daftar tombol pop-up. Seperti tombol utama, tombol ini transparan hingga diarahkan atau berinteraksi dengannya, dan diregangkan untuk mengisi.

Bagian panah dari tombol terpisah yang digunakan untuk memicu pop-up.

.gui-popup-button {
  inline-size: 4ch;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-inline-start: var(--border);
  border-start-end-radius: var(--radius);
  border-end-end-radius: var(--radius);
}

Lapiskan status pengarahan kursor, fokus, dan aktif dengan Penetasan CSS dan pemilih fungsional :is():

.gui-popup-button {
  

  &:is(:hover,:focus-within) {
    background: var(--theme-hover);
  }

  /* fixes iOS trying to be helpful */
  &:focus {
    outline: none;
  }

  &:active {
    background: var(--theme-active);
  }
}

Gaya ini adalah hook utama untuk menampilkan dan menyembunyikan pop-up. Jika .gui-popup-button memiliki focus pada turunannya, tetapkan opacity, posisi, dan pointer-events, pada ikon dan pop-up.

.gui-popup-button {
  

  &:focus-within {
    & > svg {
      transition-duration: var(--in-speed);
      transform: rotateZ(.5turn);
    }
    & > .gui-popup {
      transition-duration: var(--in-speed);
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }
  }
}

Setelah gaya masuk dan keluar selesai, bagian terakhir adalah mengubah transisi secara kondisional bergantung pada preferensi gerakan pengguna:

.gui-popup-button {
  

  @media (--motionOK) {
    & > svg {
      transition: transform var(--out-speed) ease;
    }
    & > .gui-popup {
      transform: translateY(5px);

      transition:
        opacity var(--out-speed) ease,
        transform var(--out-speed) ease;
    }
  }
}

Jika Anda jeli, Anda akan melihat bahwa opasitas masih ditransisikan untuk pengguna yang lebih memilih gerakan yang dikurangi.

Menata gaya pop-up

Elemen .gui-popup adalah daftar tombol kartu mengambang yang menggunakan properti kustom dan unit relatif agar lebih kecil, dicocokkan secara interaktif dengan tombol utama, dan sesuai dengan merek dengan penggunaan warnanya. Perhatikan bahwa ikon memiliki lebih sedikit kontras, lebih tipis, dan bayangan memiliki sedikit warna biru merek. Seperti halnya tombol, UI dan UX yang kuat adalah hasil dari detail kecil yang menumpuk.

Elemen kartu mengambang.

.gui-popup {
  --shadow: 220 70% 15%;
  --shadow-strength: 1%;

  opacity: 0;
  pointer-events: none;

  position: absolute;
  bottom: 80%;
  left: -1.5ch;

  list-style-type: none;
  background: var(--popupbg);
  color: var(--theme-text);
  padding-inline: 0;
  padding-block: .5ch;
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  font-size: .9em;
  transition: opacity var(--out-speed) ease;

  box-shadow:
    0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
    0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
    0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
    0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
    0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
    0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
  ;
}

Ikon dan tombol diberi warna merek untuk menata gaya dengan baik dalam setiap kartu tema gelap dan terang:

Link dan ikon untuk checkout, Pembayaran Cepat, dan Simpan untuk nanti.

.gui-popup {
  

  & svg {
    fill: var(--popupbg);
    stroke: var(--theme);

    @media (prefers-color-scheme: dark) {
      stroke: var(--theme-border);
    }
  }

  & button {
    color: var(--theme-text);
    width: 100%;
  }
}

Pop-up tema gelap memiliki tambahan bayangan teks dan ikon, serta bayangan kotak yang sedikit lebih intens:

Pop-up dalam tema gelap.

.gui-popup {
  

  @media (--dark) {
    --shadow-strength: 5%;
    --shadow: 220 3% 2%;

    & button:not(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--ontheme);
    }

    & button:not(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--ontheme));
    }
  }
}

Gaya ikon <svg> umum

Semua ikon berukuran relatif terhadap tombol font-size tempatnya digunakan dengan menggunakan unit ch sebagai inline-size. Masing-masing juga diberi beberapa gaya untuk membantu membuat ikon garis batas yang lembut dan halus.

.gui-split-button svg {
  inline-size: 2ch;
  box-sizing: content-box;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2px;
}

Tata letak kanan-ke-kiri

Properti logis melakukan semua pekerjaan yang rumit. Berikut adalah daftar properti logis yang digunakan: - display: inline-flex membuat elemen flex inline. - padding-block dan padding-inline sebagai pasangan, bukan singkatan padding, mendapatkan manfaat padding sisi logis. - border-end-start-radius dan teman akan membulatkan sudut berdasarkan arah dokumen. - inline-size, bukan width, memastikan ukuran tidak terikat dengan dimensi fisik. - border-inline-start menambahkan batas ke awal, yang mungkin berada di sebelah kanan atau kiri, bergantung pada arah skrip.

JavaScript

Hampir semua JavaScript berikut bertujuan untuk meningkatkan aksesibilitas. Dua library helper saya digunakan untuk mempermudah tugas. BlingBlingJS digunakan untuk kueri DOM yang ringkas dan penyiapan pemroses peristiwa yang mudah, sedangkan roving-ux membantu memfasilitasi interaksi keyboard dan gamepad yang dapat diakses untuk pop-up.

import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'

const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')

Dengan library di atas yang diimpor dan elemen yang dipilih serta disimpan ke dalam variabel, mengupgrade pengalaman hanya tinggal beberapa fungsi lagi.

Indeks keliling

Saat keyboard atau pembaca layar memfokuskan .gui-popup-button, kita ingin meneruskan fokus ke tombol pertama (atau yang terakhir difokuskan) di .gui-popup. Library ini membantu kita melakukannya dengan parameter element dan target.

popupButtons.forEach(element =>
  rovingIndex({
    element,
    target: 'button',
  }))

Elemen kini meneruskan fokus ke turunan <button> target dan mengaktifkan navigasi tombol panah standar untuk menjelajahi opsi.

Mengaktifkan/menonaktifkan aria-expanded

Meskipun secara visual terlihat bahwa pop-up ditampilkan dan disembunyikan, pembaca layar memerlukan lebih dari sekadar isyarat visual. JavaScript digunakan di sini untuk melengkapi interaksi :focus-within yang didorong CSS dengan mengalihkan atribut yang sesuai untuk pembaca layar.

popupButtons.on('focusin', e => {
  e.currentTarget.setAttribute('aria-expanded', true)
})

popupButtons.on('focusout', e => {
  e.currentTarget.setAttribute('aria-expanded', false)
})

Mengaktifkan kunci Escape

Fokus pengguna telah sengaja dikirim ke perangkap, yang berarti kita perlu memberikan cara untuk keluar. Cara yang paling umum adalah mengizinkan penggunaan kunci Escape. Untuk melakukannya, perhatikan penekanan tombol pada tombol pop-up, karena setiap peristiwa keyboard pada turunan akan muncul ke induk ini.

popupButtons.on('keyup', e => {
  if (e.code === 'Escape')
    e.target.blur()
})

Jika tombol pop-up melihat penekanan tombol Escape, tombol tersebut akan menghapus fokus dari dirinya sendiri dengan blur().

Klik tombol terpisah

Terakhir, jika pengguna mengklik, mengetuk, atau keyboard berinteraksi dengan tombol, aplikasi perlu melakukan tindakan yang sesuai. Pembentukan gelembung peristiwa digunakan lagi di sini, tetapi kali ini pada penampung .gui-split-button, untuk menangkap klik tombol dari pop-up turunan atau tindakan utama.

splitButtons.on('click', event => {
  if (event.target.nodeName !== 'BUTTON') return
  console.info(event.target.innerText)
})

Kesimpulan

Setelah Anda tahu cara saya melakukannya, bagaimana Anda melakukannya‽ 🙂

Mari kita diversifikasi pendekatan dan pelajari semua cara untuk mem-build di web. Buat demo, tweet link-nya, dan saya akan menambahkannya ke bagian remix komunitas di bawah.

Remix komunitas