Membuat komponen tombol terpisah

Ringkasan dasar tentang cara membuat komponen tombol terpisah yang dapat diakses.

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

Demo

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

Ringkasan

Tombol terpisah adalah tombol yang menyembunyikan tombol utama dan daftar tombol tambahan. {i>Tool<i} berguna untuk mengekspos tindakan umum sambil menyarangkan data sekunder, yang lebih jarang digunakan tindakan hingga diperlukan. Tombol terpisah bisa sangat penting untuk membantu desain yang sibuk terasa minimal. Tombol pemisahan lanjutan bahkan dapat mengingat tindakan terakhir pengguna dan mempromosikannya pada posisi utama.

Tombol pemisahan umum dapat ditemukan di aplikasi email Anda. Tindakan utama dikirim, namun mungkin Anda dapat mengirimnya nanti atau menyimpan draf sebagai gantinya:

Contoh tombol pemisahan seperti yang terlihat di aplikasi email.

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

Suku cadang

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

Elemen HTML yang membentuk tombol pemisahan.

Penampung tombol pemisahan 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 dalam container dengan dua bentuk sudut yang sesuai untuk fokus, arahkan kursor dan interaksi aktif ke muncul dalam .gui-split-button.

Pemeriksa menunjukkan aturan CSS untuk elemen tombol.

Tombol pop-up

"Tombol pop-up" {i>support<i} adalah untuk mengaktifkan dan menunjukkan daftar tombol sekunder. Perhatikan bahwa ini bukan <button> dan tidak dapat difokuskan. Namun, ini merupakan 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-nya .gui-popup-button, diposisikan absolut dan yang menggabungkan daftar tombol secara semantik.

Pemeriksa menunjukkan aturan CSS untuk class gui-popup

Tindakan sekunder

<button> yang dapat difokuskan dengan ukuran font yang sedikit lebih kecil dari warna utama tombol tindakan dilengkapi ikon dan gaya ke tombol utama.

Pemeriksa menunjukkan aturan CSS untuk elemen tombol.

Properti kustom

Variabel-variabel berikut membantu dalam menciptakan keselarasan warna dan pusat untuk memodifikasi 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 adalah penting bagi pembaca layar untuk mengetahui kemampuan dan status pemisahan pengalaman tombol. 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 akan memperluasnya. Satu-satunya yang menarik dengan strategi ini adalah .gui-split-button container tidak dapat menggunakan overflow: hidden, karena akan memotong pop-up agar tidak disajikan secara visual.

<ul> yang diisi dengan konten <li><button> akan mengumumkan dirinya sebagai "tombol "daftar" bagi pembaca layar, tepatnya 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 gaya dan bersenang-senang dengan warna, saya telah menambahkan ikon ke tombol sekunder dari https://heroicons.com. Ikon bersifat opsional untuk keduanya tombol primer 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 pada tempatnya, gaya siap untuk memberikan warna dan tata letak.

Menata gaya penampung tombol pemisahan

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 pisahkan.

Gaya <button>

Tombol sangat baik dalam menyamarkan berapa banyak kode yang diperlukan. Anda mungkin perlu membatalkan atau mengganti gaya {i>default<i} browser, tetapi Anda juga harus menerapkan beberapa pewarisan, menambahkan status interaksi dan beradaptasi dengan berbagai preferensi pengguna, input. Gaya tombol bertambah dengan cepat.

Tombol-tombol ini berbeda dari tombol biasa karena berbagi latar belakang dengan elemen induk. Biasanya, tombol memiliki warna latar belakang dan teksnya. Namun, 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;
}

Menambahkan status interaksi dengan beberapa CSS kelas semu dan penggunaan pencocokan properti khusus 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 melengkapi 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 gaya, tombol tema terang dan ikon mendapatkan shadow:

.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 mudah diakses, tetapi ia memiliki satu {i>downfall<i}: tidak cerdas tentang apakah pengguna perlu melihatnya atau tidak tidak, itu akan diterapkan untuk fokus apa pun.

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

Menata gaya tombol pop-up

Flexbox 4ch untuk menempatkan ikon di tengah dan menambatkan daftar tombol pop-up. Suka tombol utama, akan tetap transparan hingga kursor diarahkan ke atasnya atau berinteraksi dengannya dan direntangkan hingga penuh.

Bagian panah pada tombol pemisahan 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 saat pengarahan kursor, fokus, dan status aktif dengan CSS Nesting dan Pemilih fungsi :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 di 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 kondisional transformasi transisi 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;
    }
  }
}

Perhatikan dengan cermat kode tersebut akan melihat bahwa opasitas masih dialihkan untuk pengguna yang lebih suka mengurangi gerakan.

Menata gaya pop-up

Elemen .gui-popup adalah daftar tombol kartu mengambang menggunakan properti kustom dan unit relatif menjadi lebih kecil, secara interaktif sesuai dengan tombol, dan merek dengan penggunaan warna. Perhatikan ikon-ikon tersebut lebih sedikit kontras, lebih tipis, dan bayangannya memiliki sentuhan biru merek. Seperti pada tombol, UI dan UX yang kuat adalah hasil dari menumpuk 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 untuk ditata dengan baik dalam setiap warna gelap dan kartu bertema terang:

Link dan ikon untuk checkout, Quick Pay, 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 teks dan bayangan ikon, plus bayangan kotak yang 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 memiliki ukuran relatif dengan tombol font-size yang digunakan di dalamnya menggunakan unit ch sebagai inline-size. Masing-masing juga diberi beberapa gaya untuk membantu menguraikan ikon secara lembut dan lancar.

.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 tugas yang kompleks. Berikut adalah daftar properti logis yang digunakan: - display: inline-flex membuat elemen fleksibel inline. - padding-block dan padding-inline sebagai pasangan, bukan padding secara singkat, dapatkan manfaat memberi padding pada sisi logis. - border-end-start-radius dan teman akan sudut membulat berdasarkan arah dokumen. - inline-size, bukan width, memastikan ukuran tidak terikat dengan dimensi fisik. - border-inline-start menambahkan batas ke awal, yang mungkin di kanan atau kiri bergantung pada arah skrip.

JavaScript

Hampir semua JavaScript berikut adalah untuk meningkatkan aksesibilitas. Dua dari {i>help library<i} digunakan untuk membuat tugas lebih mudah. BlingBlingJS digunakan untuk ringkasan Kueri DOM dan penyiapan pemroses peristiwa yang mudah, sekaligus roving-ux membantu memfasilitasi aksesibilitas interaksi keyboard dan gamepad 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 telah diimpor serta elemen yang dipilih dan disimpan ke dalam variabel, mengupgrade pengalaman tinggal beberapa fungsi lagi untuk diselesaikan.

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

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

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

Mengalihkan aria-expanded

Meskipun terlihat jelas bahwa pop-up muncul dan tersembunyi, pembaca layar membutuhkan lebih dari sekadar isyarat visual. JavaScript digunakan di sini untuk melengkapi interaksi :focus-within berbasis CSS dengan mengalihkan atribut yang sesuai pada pembaca layar.

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

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

Mengaktifkan tombol Escape

Fokus pengguna sengaja diarahkan ke jebakan, yang berarti kita perlu memberi cara untuk pergi. Cara yang paling umum adalah mengizinkan penggunaan tombol Escape. Untuk melakukannya, perhatikan penekanan tombol pada tombol {i>pop-up<i}, karena setiap peristiwa {i>keyboard<i} aktif turunan akan muncul di induk ini.

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

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

Klik tombol pisahkan

Terakhir, jika pengguna mengeklik, mengetuk, atau {i>keyboard<i} berinteraksi dengan tombol, maka aplikasi perlu melakukan tindakan yang sesuai. Gelembung acara digunakan sekali lagi di sini, tetapi kali ini di container .gui-split-button, untuk menangkap tombol dari pop-up turunan atau tindakan utama.

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

Kesimpulan

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