Ringkasan dasar tentang cara mem-build komponen tombol aksesibel dan responsif.
Dalam postingan ini,
Jika Anda lebih suka menonton video,
Ringkasan
Fungsi switch 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-opsinya..
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 ini 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
normal 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
tumpukan di atas trek,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 jempol
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
dicentang.
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 arahkan kursor akan 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 khusus,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
}
Masukan 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 {i>switch<i} yang sangat kecil ini akhirnya menjadi pekerjaan
paling banyak dari semua Tantangan GUI sejauh ini! Sekarang setelah Anda tahu bagaimana saya melakukannya,
Mari kita diversifikasi pendekatan kami dan mempelajari semua cara untuk membangun di web.
Remix komunitas
- @KonstantinRouda dengan elemen kustom: demo dan code.
- @jhvanderschee dengan tombol: Codepen.
Resource
Temukan .
kode sumber di GitHub.