Membuat komponen tombol terpisah

Ringkasan dasar tentang cara membangun komponen tombol terpisah yang mudah diakses.

Dalam postingan ini saya ingin berbagi pemikiran tentang cara membuat tombol terpisah . Coba demonya.

Demo

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

Ringkasan

Tombol terpisah adalah tombol yang menyembunyikan tombol utama dan daftar tombol tambahan. Fungsi ini berguna untuk mengekspos tindakan umum sekaligus membuat tingkat tindakan sekunder yang jarang digunakan hingga diperlukan. Tombol terpisah bisa jadi sangat penting untuk membantu meminimalkan kesan desain yang sibuk. Tombol pemisahan lanjutan bahkan dapat mengingat tindakan pengguna terakhir dan mempromosikannya ke posisi utama.

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

Contoh tombol pemisahan seperti yang terlihat dalam aplikasi email.

Area tindakan bersama itu bagus, karena pengguna tidak perlu melihat-lihat. Mereka tahu bahwa tindakan email penting terdapat dalam tombol pisahkan.

Suku cadang

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

Elemen HTML yang membentuk tombol pemisahan.

Penampung tombol terpisah tingkat atas

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

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

Tombol tindakan utama

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

Pemeriksa menampilkan aturan CSS untuk elemen tombol.

Tombol pop-up

Elemen dukungan "tombol popup" 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.

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

Pemeriksa yang menunjukkan aturan CSS untuk class gui-popup

Tindakan sekunder

<button> yang dapat difokuskan dengan ukuran font yang sedikit lebih kecil dari tombol tindakan utama menampilkan ikon dan gaya gratis untuk tombol utama.

Pemeriksa menampilkan aturan CSS untuk elemen tombol.

Properti khusus

Variabel berikut membantu menciptakan harmoni warna dan tempat terpusat 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 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 tangkapan dengan strategi ini adalah penampung .gui-split-button tidak dapat menggunakan overflow: hidden, karena akan memotong pop-up agar tidak ada secara visual.

<ul> yang diisi dengan konten <li><button> akan mengumumkan dirinya sebagai "daftar tombol" untuk pembaca layar, yang merupakan antarmuka yang sedang 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 gaya dan untuk 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 ada, gaya siap untuk 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 baik dalam menyamarkan berapa banyak kode yang diperlukan. Anda mungkin perlu mengurungkan atau mengganti gaya default browser, tetapi Anda juga harus menerapkan beberapa warisan, menambahkan status interaksi, dan beradaptasi dengan berbagai preferensi pengguna dan jenis input. Gaya tombol bertambah dengan cepat.

Tombol ini berbeda dari tombol biasa karena memiliki latar belakang dengan elemen induk. Biasanya, tombol memiliki warna latar belakang dan teksnya. Namun demikian, mereka membagikannya, dan hanya menerapkan latar belakang mereka 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 khusus yang cocok untuk status tersebut:

.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 gaya, 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));
    }
  }
}

Sebuah tombol yang hebat 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 mudah diakses, tetapi memiliki satu kelemahan: tidaklah cerdas apakah pengguna perlu melihatnya atau tidak, ini akan berlaku untuk fokus apa pun.

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

Menata gaya tombol pop-up

Flexbox 4ch untuk menempatkan ikon di tengah dan menambatkan daftar tombol pop-up. Seperti tombol utama, tombol ini transparan sampai diarahkan kursor atau berinteraksi dengan tombol, dan direntangkan untuk mengisi.

Bagian panah dari tombol pisahkan 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);
}

Lapisan dalam status pengarahan kursor, fokus, dan status aktif dengan CSS Nesting 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 salah satu 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 transformasi transisi bersyarat 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;
    }
  }
}

Kajian yang mendalam pada kode akan melihat opasitas masih dialihkan untuk pengguna yang lebih memilih gerakan yang dikurangi.

Menata gaya pop-up

Elemen .gui-popup adalah daftar tombol kartu mengambang yang menggunakan properti khusus dan unit relatif agar lebih kecil secara interaktif, cocok dengan tombol utama, dan pada merek dengan penggunaan warnanya. Perhatikan ikon yang memiliki kontras yang lebih sedikit, lebih tipis, dan bayangannya memiliki sedikit sentuhan warna biru. Seperti halnya tombol, UI dan UX yang kuat adalah hasil dari penumpukan detail kecil ini.

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 agar gayanya pas dalam setiap kartu bertema 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 penambahan bayangan teks dan ikon, ditambah 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 sesuai dengan tombol font-size yang digunakannya dengan menggunakan unit ch sebagai inline-size. Masing-masing juga diberi beberapa gaya untuk membantu menguraikan ikon secara halus 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 kompleks. Berikut adalah daftar properti logis yang digunakan: - display: inline-flex membuat elemen fleksibel inline. - padding-block dan padding-inline sebagai pasangan, bukan singkatan padding, dapatkan manfaat padding sisi logis. - border-end-start-radius dan friends 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 kanan atau kiri, bergantung pada arah skrip.

JavaScript

Hampir semua JavaScript berikut digunakan untuk meningkatkan aksesibilitas. Dua pustaka 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 mudah diakses untuk pop-up.

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

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

Setelah library di atas diimpor dan elemen yang dipilih serta disimpan ke dalam variabel, tinggal mengupgrade pengalaman menjadi beberapa fungsi lagi yang belum selesai.

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 melakukan hal ini dengan parameter element dan target.

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

Elemen ini sekarang meneruskan fokus ke turunan <button> target dan memungkinkan navigasi tombol panah standar untuk menjelajahi opsi.

Mengalihkan tombol aria-expanded

Meskipun terlihat jelas bahwa pop-up ditampilkan dan disembunyikan, pembaca layar membutuhkan lebih dari sekadar isyarat visual. JavaScript digunakan di sini untuk melengkapi interaksi :focus-within berbasis CSS dengan mengalihkan atribut yang sesuai 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 sengaja diarahkan ke perangkap, yang berarti kita harus menyediakan cara untuk keluar. Cara yang paling umum adalah dengan 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 tombol Escape ditekan, fokus akan dihapus dengan blur().

Klik tombol terpisah

Terakhir, jika pengguna mengklik, mengetuk, atau keyboard berinteraksi dengan tombol, aplikasi harus melakukan tindakan yang sesuai. Balon peristiwa digunakan lagi di sini, tetapi kali ini di 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‽ 🙂

Mari lakukan diversifikasi pendekatan dan pelajari semua cara untuk membangun di web. Buat demo, link tweet me, dan saya akan menambahkannya ke bagian remix komunitas di bawah.

Remix komunitas