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.
Jika Anda lebih suka menonton video, berikut versi YouTube dari postingan ini:
Ringkasan
Tombol terpisah adalah tombol yang menyembunyikan tombol utama dan daftar tombol tambahan. Tindakan ini berguna untuk mengekspos tindakan umum sambil menyusun tindakan sekunder yang lebih jarang digunakan hingga diperlukan. Tombol terpisah dapat menjadi sangat penting untuk membantu desain yang sibuk terasa minimal. Tombol split lanjutan bahkan dapat mengingat tindakan pengguna terakhir dan mempromosikannya ke posisi utama.
Tombol terpisah umum dapat ditemukan di aplikasi email Anda. Tindakan utama adalah kirim, tetapi mungkin Anda dapat mengirim nanti atau menyimpan draf:
Area tindakan bersama cukup bagus karena pengguna tidak perlu melihat-lihat. Mereka tahu bahwa tindakan email penting ada di tombol terpisah.
Suku cadang
Mari kita bahas bagian penting tombol terpisah sebelum membahas orkestrasi keseluruhannya 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 terlihat dan dapat difokuskan pada awalnya cocok dengan penampung dengan
dua bentuk sudut yang cocok untuk
fokus,
melayang, dan
aktif interaksi agar
tampak berada di dalam .gui-split-button
.
Tombol pop-up
Elemen dukungan "tombol pop-up" digunakan untuk mengaktifkan dan menyiratkan daftar tombol sekunder. Perhatikan bahwa ini bukan <button>
dan tidak dapat difokuskan. Namun,
elemen ini adalah anchor penentuan posisi 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
membungkus 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 yang melengkapi tombol utama.
Properti kustom
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 sangat penting agar pembaca layar 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 tombol yang
meluaskannya. Satu-satunya kendala dengan strategi ini adalah penampung .gui-split-button
tidak dapat menggunakan overflow: hidden
, karena akan memotong pop-up sehingga tidak
tampil 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 menambah sentuhan menarik dan bermain-main dengan warna, saya telah menambahkan ikon ke tombol sekunder dari https://heroicons.com. Ikon bersifat opsional untuk 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
Setelah HTML dan konten tersedia, gaya siap memberikan warna dan tata letak.
Menata gaya penampung tombol terpisah
Jenis tampilan inline-flex
berfungsi dengan baik untuk komponen pembungkus 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 <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 dari tombol biasa karena berbagi latar belakang dengan elemen induk. Biasanya, tombol memiliki warna latar belakang dan teksnya sendiri. Namun, chip ini membagikannya, dan hanya menerapkan latar belakangnya sendiri saat berinteraksi.
.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 gunakan 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 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 menambahkan sentuhan menarik, 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 mudah diakses, tetapi memiliki satu
kelemahan: tidak cerdas dalam menentukan apakah pengguna perlu melihatnya atau
tidak, dan akan berlaku untuk fokus apa pun.
Video di bawah berupaya menguraikan mikrointeraksi ini, untuk menunjukkan bagaimana :focus-visible
menjadi alternatif yang cerdas.
Menata gaya tombol pop-up
Kotak 4ch
flexbox untuk memusatkan ikon dan menyematkan daftar tombol pop-up. Seperti
tombol utama, tombol ini transparan hingga kursor diarahkan ke atasnya atau berinteraksi
dengannya, dan direntangkan 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);
}
Lapisan dalam status melayang, fokus, dan 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 pengait 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 transisi transformasi 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;
}
}
}
Mata yang cermat pada kode 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 floating yang menggunakan properti kustom
dan unit relatif agar sedikit lebih kecil, cocok secara interaktif dengan tombol
utama, dan sesuai merek dengan penggunaan warnanya. Perhatikan bahwa ikon memiliki kontras yang lebih rendah,
lebih tipis, dan bayangannya memiliki sentuhan warna biru merek. Seperti tombol,
UI dan UX yang kuat adalah hasil dari detail kecil yang bertumpuk ini.
.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 terlihat bagus 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 bayangan teks dan 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>
generik
Semua ikon berukuran relatif terhadap tombol font-size
yang digunakan di dalamnya dengan
menggunakan unit ch
sebagai
inline-size
. Setiap ikon juga diberi beberapa gaya untuk membantu menguraikan ikon dengan 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 pekerjaan yang rumit.
Berikut daftar properti logis yang digunakan:
- display: inline-flex
membuat elemen flex inline.
- padding-block
dan padding-inline
sebagai pasangan, bukan singkatan padding
, mendapatkan manfaat dari padding sisi logis.
- border-end-start-radius
dan
teman akan
membuat sudut bulat berdasarkan arah dokumen.
- inline-size
, bukan width
, memastikan ukuran tidak terikat pada 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 adalah untuk meningkatkan aksesibilitas. Dua library pembantu saya digunakan untuk mempermudah tugas. BlingBlingJS digunakan untuk kueri DOM yang ringkas dan penyiapan pemroses peristiwa yang mudah, sementara 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 serta elemen yang dipilih dan disimpan ke dalam variabel, peningkatan pengalaman hanya memerlukan beberapa fungsi lagi hingga selesai.
Indeks roaming
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 ini kini meneruskan fokus ke turunan <button>
target dan memungkinkan
navigasi tombol panah standar untuk menjelajahi opsi.
Mengalihkan aria-expanded
Meskipun secara visual terlihat bahwa pop-up ditampilkan dan disembunyikan, pembaca layar memerlukan lebih dari sekadar petunjuk visual. JavaScript digunakan di sini untuk melengkapi interaksi :focus-within
yang didorong CSS dengan mengganti 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 tombol Escape
Fokus pengguna telah dikirim secara sengaja ke jebakan, yang berarti kita perlu
menyediakan cara untuk keluar. Cara yang paling umum adalah dengan mengizinkan penggunaan kunci Escape
.
Untuk melakukannya, perhatikan penekanan tombol pada tombol pop-up, karena semua 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 berinteraksi dengan tombol menggunakan keyboard, aplikasi harus melakukan tindakan yang sesuai. Bubbling 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
Sekarang setelah Anda tahu cara saya melakukannya, bagaimana Anda‽ 🙂
Mari kita diversifikasi pendekatan kita dan pelajari semua cara untuk membangun di web. Buat demo, tweet linknya kepada saya, dan saya akan menambahkannya ke bagian remix komunitas di bawah.