Ringkasan dasar tentang cara mem-build komponen tombol aksesibel dan responsif.
Dalam postingan ini,
Jika Anda lebih suka video,
Ringkasan
Tombol berfungsi mirip dengan kotak centang,
Demo ini menggunakan <input type="checkbox" role="switch">
untuk sebagian besar
fungsinya,
Properti kustom
Variabel berikut mewakili berbagai bagian tombol dan
opsi tombol..
berisi properti kustom yang digunakan
di seluruh turunan komponen,
Lacak
Panjang (--track-size
),
. gui-switch {
--track-size: calc(var(--thumb-size) * 2);
--track-padding: 2px;
--track-inactive: hsl(80 0% 80%);
--track-active: hsl(80 60% 45%);
--track-color-inactive: var(--track-inactive);
--track-color-active: var(--track-active);
@media (prefers-color-scheme: dark) {
--track-inactive: hsl(80 0% 35%);
--track-active: hsl(80 60% 60%);
}
}
Kalimba
Ukuran,
. gui-switch {
--thumb-size: 2rem;
--thumb: hsl(0 0% 100%);
--thumb-highlight: hsl(0 0% 0% / 25%);
--thumb-color: var(--thumb);
--thumb-color-highlight: var(--thumb-highlight);
@media (prefers-color-scheme: dark) {
--thumb: hsl(0 0% 5%);
--thumb-highlight: hsl(0 0% 100% / 25%);
}
}
Pengurangan gerakan
Untuk menambahkan alias yang jelas dan mengurangi pengulangan,
@custom-media --motionOK (prefers-reduced-motion: no-preference);
Markup
Saya memilih untuk menggabungkan elemen <input type="checkbox" role="switch">
dengan
<label>
,
<label for="switch" class="gui-switch">
Label text
<input type="checkbox" role="switch" id="switch">
</label>
<input type="checkbox">
sudah di-build sebelumnya dengan
API
dan status.checked
dan peristiwa
input
seperti oninput
dan onchanged
.
Tata letak
Flexbox,
.gui-switch
Tata letak tingkat atas untuk tombol adalah flexbox..
berisi
properti kustom pribadi dan publik yang digunakan turunan untuk menghitung
tata letak.
. gui-switch {
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
}
Memperluas dan mengubah tata letak flexbox sama seperti mengubah tata letak flexbox lainnya.flex-direction
:
<label for="light-switch" class="gui-switch" style="flex-direction: column">
Default
<input type="checkbox" role="switch" id="light-switch">
</label>
Lacak
Input kotak centang ditata gayanya sebagai jalur tombol dengan menghapus
appearance: checkbox
normalnya dan menyediakan ukurannya sendiri:
. gui-switch > input {
appearance: none;
inline-size: var(--track-size);
block-size: var(--thumb-size);
padding: var(--track-padding);
flex-shrink: 0;
display: grid;
align-items: center;
grid: [track] 1fr / [track] 1fr;
}
Jalur juga membuat area jalur petak sel tunggal satu per satu untuk ibu jari
yang akan diklaim.
Kalimba
Gaya appearance: none
juga menghapus tanda centang visual yang disediakan oleh browser.:checked
pseudo-class pada input untuk
mengganti indikator visual ini.
Thumb adalah turunan elemen pseudo yang dilampirkan ke input[type="checkbox"]
dan
ditumpuk di atas jalur,track
:
. gui-switch > input:: before {
content: "";
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
}
Gaya
Properti kustom memungkinkan komponen tombol serbaguna yang beradaptasi dengan skema
warna,
Gaya interaksi sentuh
Di perangkat seluler,cursor: pointer
saya sendiri:
. gui-switch {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
Tidak selalu disarankan untuk menghapus gaya tersebut,
Lacak
Gaya elemen ini sebagian besar berkaitan dengan bentuk dan warnanya,.
induk melalui
cascade.
. gui-switch > input {
appearance: none;
border: none;
outline-offset: 5px;
box-sizing: content-box;
padding: var(--track-padding);
background: var(--track-color-inactive);
inline-size: var(--track-size);
block-size: var(--thumb-size);
border-radius: var(--track-size);
}
Berbagai opsi penyesuaian untuk jalur tombol berasal dari empat
properti kustom.border: none
ditambahkan karena appearance: none
tidak
menghapus batas dari kotak centang di semua browser.
Kalimba
Elemen thumb sudah berada di track
kanan,
. gui-switch > input:: before {
background: var(--thumb-color);
border-radius: 50%;
}
Interaksi
Gunakan properti kustom untuk mempersiapkan interaksi yang akan menampilkan tanda kursor
dan perubahan posisi ibu jari.
. gui-switch > input:: before {
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25 s ease;
}}
}
Posisi ibu jari
Properti kustom menyediakan mekanisme sumber tunggal untuk memosisikan thumb di
jalur.0%
dan 100%
.
Elemen input
memiliki variabel posisi --thumb-position
,translateX
:
. gui-switch > input {
--thumb-position: 0%;
}
. gui-switch > input:: before {
transform: translateX(var(--thumb-position));
}
Sekarang kita bebas mengubah --thumb-position
dari CSS dan pseudo-class
yang disediakan di elemen kotak centang.transition: transform
var(
secara kondisional sebelumnya pada elemen ini,
/* positioned at the end of the track: track length - 100% (thumb width) */
. gui-switch > input:checked {
--thumb-position: calc(var(--track-size) - 100%);
}
/* positioned in the center of the track: half the track - half the thumb */
. gui-switch > input:indeterminate {
--thumb-position: calc(
(var(--track-size) / 2) - (var(--thumb-size) / 2)
);
}
Saya rasa orkestrasi yang dipisahkan ini berhasil dengan baik.translateX
.
Vertikal
Dukungan dilakukan dengan class pengubah -vertical
yang menambahkan rotasi dengan
transformasi CSS ke elemen input
.
Namun,--track-size
dan
--track-padding
.
. gui-switch. -vertical {
min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));
& > input {
transform: rotate(-90deg);
}
}
(RTL) kanan ke kiri
Seorang teman CSS,
. gui-switch {
--isLTR: 1;
&:dir(rtl) {
--isLTR: -1;
}
}
Properti kustom yang disebut --isLTR
awalnya menyimpan nilai 1
,true
karena tata letak kita adalah dari kiri ke kanan secara default.:dir()
,-1
saat komponen berada dalam tata letak kanan ke kiri.
Terapkan --isLTR
dengan menggunakannya dalam calc()
di dalam transformasi:
. gui-switch.- vertical > input {
transform: rotate(-90deg);
transform: rotate(calc(90deg * var(--isLTR) * -1));
}
Sekarang rotasi tombol vertikal memperhitungkan posisi sisi yang berlawanan
yang diperlukan oleh tata letak kanan-ke-kiri.
Transformasi translateX
pada pseudo-elemen thumb juga perlu diperbarui untuk
mempertimbangkan persyaratan sisi yang berlawanan:
. gui-switch > input:checked {
--thumb-position: calc(var(--track-size) - 100%);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
. gui-switch > input:indeterminate {
--thumb-position: calc(
(var(--track-size) / 2) - (var(--thumb-size) / 2)
);
--thumb-position: calc(
((var(--track-size) / 2) - (var(--thumb-size) / 2))
* var(--isLTR)
);
}
Meskipun pendekatan ini tidak akan berfungsi untuk memenuhi semua kebutuhan terkait konsep seperti transformasi CSS
logis,
Negara bagian
Penggunaan input[type="checkbox"]
bawaan tidak akan lengkap tanpa
menangani berbagai status yang dapat dimilikinya: :checked
,:disabled
,:indeterminate
,:hover
.:focus
sengaja dibiarkan,
Dicentang
<label for="switch-checked" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>
Status ini mewakili status on
.
. gui-switch > input:checked {
background: var(--track-color-active);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
Nonaktif
<label for="switch-disabled" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>
Tombol :disabled
tidak hanya terlihat berbeda secara visual,appearance: none
.
. gui-switch > input:disabled {
cursor: not-allowed;
--thumb-color: transparent;
&::before {
cursor: not-allowed;
box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);
@media (prefers-color-scheme: dark) { & {
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
}}
}
}
Status ini rumit karena memerlukan tema gelap dan terang dengan status dinonaktifkan dan
diperiksa.
Tidak tentu
Status yang sering dilupakan adalah :indeterminate
,
Menyetel kotak centang ke tidak ditentukan cukup sulit,
<label for="switch-indeterminate" class="gui-switch">
Indeterminate
<input type="checkbox" role="switch" id="switch-indeterminate">
<script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>
Karena statusnya,
. gui-switch > input:indeterminate {
--thumb-position: calc(
calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
* var(--isLTR)
);
}
Arahkan kursor
Interaksi pengarahan kursor harus memberikan dukungan visual untuk UI yang terhubung dan juga
memberikan arah ke UI interaktif.
Efek "sorotan" dilakukan dengan box-shadow
.--highlight-size
.box-shadow
dan melihatnya tumbuh,
. gui-switch > input:: before {
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow . 25s ease;
}}
}
. gui-switch > input:not(:disabled):hover:: before {
--highlight-size: . 5rem;
}
JavaScript
Bagi saya,
Jempol yang dapat ditarik
Pseudo-elemen thumb menerima posisinya dari var(
yang dicakup .
,--thumb-position
.
Karena komponen sudah berfungsi 100% sebelum skrip ini muncul,
touch-action
Menarik adalah gestur,touch-action
.touch-action
,
CSS berikut menginstruksikan browser bahwa saat gestur pointer dimulai dari
dalam jalur tombol ini,
. gui-switch > input {
touch-action: pan-y;
}
Hasil yang diinginkan adalah gestur horizontal yang juga tidak menggeser atau men-scroll
halaman.
Utilitas gaya nilai piksel
Saat penyiapan dan selama tarik,getStyle(
ini.
const getStyle = (element, prop) => {
return parseInt(window. getComputedStyle(element). getPropertyValue(prop));
}
const getPseudoStyle = (element, prop) => {
return parseInt(window. getComputedStyle(element, ':before'). getPropertyValue(prop));
}
export {
getStyle,
getPseudoStyle,
}
Perhatikan bagaimana window.
menerima argumen kedua,
dragging
Ini adalah momen inti untuk logika tarik dan ada beberapa hal yang perlu diperhatikan dari pengendali peristiwa fungsi:
const dragging = event => {
if (!state. activethumb) return
let {thumbsize, bounds, padding} = switches. get(state. activethumb. parentElement)
let directionality = getStyle(state. activethumb, '--isLTR')
let track = (directionality === -1)
? (state. activethumb. clientWidth * -1) + thumbsize + padding
: 0
let pos = Math. round(event. offsetX - thumbsize / 2)
if (pos < bounds. lower) pos = 0
if (pos > bounds. upper) pos = bounds. upper
state. activethumb. style. setProperty('--thumb-position', `${track + pos}px`)
}
Hero skrip adalah state.
,switches
adalah Map()
dengan
kunci .
dan nilai adalah batas dan ukuran yang di-cache yang membuat
skrip tetap efisien.--isLTR
,event.
juga berharga,
state. activethumb. style. setProperty('--thumb-position', `${track + pos}px`)
Baris terakhir CSS ini menetapkan properti kustom yang digunakan oleh elemen thumb.--thumb-transition-duration
ke 0s
untuk sementara,
dragEnd
Agar pengguna diizinkan untuk menarik jauh di luar tombol dan melepaskannya,
window. addEventListener('pointerup', event => {
if (!state. activethumb) return
dragEnd(event)
})
Menurut saya,
const dragEnd = event => {
if (!state. activethumb) return
state. activethumb. checked = determineChecked()
if (state. activethumb. indeterminate)
state. activethumb. indeterminate = false
state. activethumb. style. removeProperty('--thumb-transition-duration')
state. activethumb. style. removeProperty('--thumb-position')
state. activethumb. removeEventListener('pointermove', dragging)
state. activethumb = null
padRelease()
}
Interaksi dengan elemen telah selesai,state.
.
determineChecked()
Fungsi ini,dragEnd
,
const determineChecked = () => {
let {bounds} = switches. get(state. activethumb. parentElement)
let curpos =
Math. abs(
parseInt(
state. activethumb. style. getPropertyValue('--thumb-position')))
if (!curpos) {
curpos = state. activethumb. checked
? bounds. lower
: bounds. upper
}
return curpos >= bounds. middle
}
Pikiran tambahan
Gestur tarik menimbulkan sedikit utang kode karena struktur HTML awal
yang dipilih,dragEnd
,padRelease()
sebagai fungsi yang terdengar aneh.
const padRelease = () => {
state. recentlyDragged = true
setTimeout(_ => {
state. recentlyDragged = false
}, 300)
}
Hal ini untuk memperhitungkan label yang mendapatkan klik ini nanti,
Jika harus melakukannya lagi,
JavaScript jenis ini adalah yang paling tidak saya sukai untuk ditulis,
const preventBubbles = event => {
if (state. recentlyDragged)
event. preventDefault() && event. stopPropagation()
}
Kesimpulan
Komponen tombol kecil ini ternyata menjadi pekerjaan paling banyak dari semua Tantangan GUI
sejauh ini.
Mari kita diversifikasi pendekatan dan pelajari semua cara untuk mem-build di web.
Remix komunitas
- @KonstantinRouda dengan elemen kustom: demo dan code.
- @jhvanderschee dengan tombol: Codepen.
Resource
Temukan kode sumber .
di GitHub.