Toggle UI Elements
Toggle switch (aan/uit). Toegankelijk met role="switch". 3 groottes, optioneel label.
src/components/ui/Toggle.astro
---
/**
* Toggle / Switch
* Toegankelijke on/off switch. Label links of rechts.
* 3 groottes, kleur via CSS vars.
*/
interface Props {
id?: string;
name?: string;
label?: string;
labelPosition?: 'left' | 'right';
checked?: boolean;
disabled?: boolean;
size?: 'sm' | 'md' | 'lg';
description?: string;
}
const {
id,
name,
label,
labelPosition = 'right',
checked = false,
disabled = false,
size = 'md',
description,
} = Astro.props;
const toggleId = id ?? `toggle-${Math.random().toString(36).slice(2, 7)}`;
---
<div class:list={['tg', `tg--${size}`, `tg--label-${labelPosition}`, { 'tg--disabled': disabled }]} data-component="toggle">
<label class="tg__label-wrap" for={toggleId}>
{label && labelPosition === 'left' && (
<div class="tg__text">
<span class="tg__label">{label}</span>
{description && <span class="tg__desc">{description}</span>}
</div>
)}
<input
type="checkbox"
id={toggleId}
name={name}
class="tg__input"
checked={checked}
disabled={disabled}
role="switch"
aria-checked={checked ? 'true' : 'false'}
/>
<span class="tg__track" aria-hidden="true">
<span class="tg__thumb"></span>
</span>
{label && labelPosition === 'right' && (
<div class="tg__text">
<span class="tg__label">{label}</span>
{description && <span class="tg__desc">{description}</span>}
</div>
)}
</label>
</div>
<script>
document.querySelectorAll('[data-component="toggle"]').forEach(el => {
const input = el.querySelector<HTMLInputElement>('.tg__input');
if (input) {
input.addEventListener('change', () => {
input.setAttribute('aria-checked', input.checked ? 'true' : 'false');
});
}
});
</script>
<style>
.tg { display: inline-block; }
.tg__label-wrap {
display: inline-flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
user-select: none;
}
.tg--disabled .tg__label-wrap { cursor: not-allowed; opacity: 0.5; }
/* Hide native checkbox */
.tg__input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
margin: 0;
}
/* Track */
.tg__track {
position: relative;
flex-shrink: 0;
background: rgba(0,0,0,0.15);
border-radius: 999px;
transition: background 0.2s;
display: flex;
align-items: center;
}
/* Sizes */
.tg--sm .tg__track { width: 32px; height: 18px; }
.tg--md .tg__track { width: 44px; height: 24px; }
.tg--lg .tg__track { width: 56px; height: 30px; }
/* Thumb */
.tg__thumb {
position: absolute;
background: #fff;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0,0,0,0.25);
transition: transform 0.2s cubic-bezier(0.4,0,0.2,1);
}
.tg--sm .tg__thumb { width: 14px; height: 14px; left: 2px; }
.tg--md .tg__thumb { width: 18px; height: 18px; left: 3px; }
.tg--lg .tg__thumb { width: 24px; height: 24px; left: 3px; }
/* Checked state */
.tg__input:checked + .tg__track {
background: var(--color-accent, #6366f1);
}
.tg--sm .tg__input:checked + .tg__track .tg__thumb { transform: translateX(14px); }
.tg--md .tg__input:checked + .tg__track .tg__thumb { transform: translateX(20px); }
.tg--lg .tg__input:checked + .tg__track .tg__thumb { transform: translateX(26px); }
/* Focus ring */
.tg__input:focus-visible + .tg__track {
outline: 2px solid var(--color-accent, #6366f1);
outline-offset: 2px;
}
/* Text */
.tg__text { display: flex; flex-direction: column; gap: 0.125rem; }
.tg__label { font-size: 0.9375rem; font-weight: 600; color: var(--color-primary, #0a0a0a); }
.tg--sm .tg__label { font-size: 0.8125rem; }
.tg--lg .tg__label { font-size: 1rem; }
.tg__desc { font-size: 0.8125rem; color: var(--color-muted, #6b7280); }
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
label | string | — | Label naast de toggle |
checked | boolean | false | Standaard ingeschakeld |
name | string | — | Form name attribuut |
disabled | boolean | false | Uitgeschakeld |
size | 'sm' | 'md' | 'lg' | 'md' | Grootte |
* = verplicht