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.
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 membuat tindakan sekunder yang lebih jarang 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:
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.
Penampung tombol terpisah tingkat atas
Komponen tingkat tertinggi adalah flexbox inline, dengan class
gui-split-button
, yang berisi tindakan utama
dan .gui-popup-button
.
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
.
Tombol 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.
Kartu pop-up
Ini adalah turunan kartu mengambang ke anchor
.gui-popup-button
, diposisikan secara absolut dan
menggabungkan daftar tombol secara semantik.
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.
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 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 sudah ada, gaya siap 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;
}
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-tombol ini berbeda dari tombol biasa karena berbagi latar belakang dengan elemen induk. Biasanya, tombol memiliki warna latar belakang dan teksnya. 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 menempatkan ikon di tengah dan menambatkan daftar tombol pop-up. Seperti
tombol utama, tombol ini transparan hingga diarahkan atau berinteraksi
dengannya, dan diregangkan untuk mengisi.
.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);
}
Tambahkan lapisan status aktif, fokus, dan pengarahan kursor 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
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 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;
}
}
}
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 kontras yang lebih rendah,
lebih tipis, dan bayangannya tampak biru. Seperti halnya tombol,
UI dan UX yang kuat adalah hasil dari detail kecil yang menumpuk.
.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 diberi gaya dengan baik dalam setiap kartu bertema gelap dan terang:
.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, serta bayangan kotak yang sedikit lebih intens:
.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 mudah 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 sengaja dikirim ke perangkap, yang berarti kita harus
menyediakan cara untuk keluar. Cara yang paling umum adalah mengizinkan penggunaan tombol 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
, fokus akan dihapus dari tombol itu sendiri
dengan
blur()
.
Klik tombol terpisah
Terakhir, jika pengguna mengklik, mengetuk, atau keyboard berinteraksi dengan tombol, aplikasi harus 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.