<div class="rtds-toggle">
<button type="button" class="rtds-toggle__switch" role="switch" aria-checked="false" id="toggle-btn">
<span class="rtds-toggle__text">Modalità scura</span>
<span class="rtds-toggle__lever" aria-hidden="true"></span>
</button>
</div>
<div class="rtds-toggle{% if classes %} {{ classes }}{% endif %}">
<button
type="button"
class="rtds-toggle__switch"
role="switch"
aria-checked="{{ 'true' if checked else 'false' }}"
id="{{ id }}"
{% if disabled %}disabled{% endif %}
{% if ariaDescribedBy %}aria-describedby="{{ ariaDescribedBy }}"{% endif %}
>
<span class="rtds-toggle__text">{{ label | safe }}</span>
<span class="rtds-toggle__lever" aria-hidden="true"></span>
{% if showStateLabels %}
<span class="rtds-toggle__state-on" aria-hidden="true">{{ onLabel if onLabel else 'On' }}</span>
<span class="rtds-toggle__state-off" aria-hidden="true">{{ offLabel if offLabel else 'Off' }}</span>
{% endif %}
</button>
</div>
{
"id": "toggle-btn",
"label": "Modalità scura"
}
/* Wrapper */
.rtds-toggle {
display: block;
}
/* =====================
Form variant
===================== */
.rtds-toggle__label {
display: inline-flex;
align-items: center;
gap: var(--spacing-3);
cursor: pointer;
}
.rtds-toggle__text {
font-size: var(--fontsize-text-base);
color: var(--color-content-01, var(--color-neutral-900));
line-height: 1.5;
}
.rtds-toggle__text abbr {
text-decoration: none;
}
.rtds-toggle__control {
position: relative;
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
/* Visually hidden but accessible and focusable — label wraps the control so clicks on __lever activate the input */
.rtds-toggle__input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Visual track */
.rtds-toggle__lever {
display: inline-block;
position: relative;
flex-shrink: 0;
width: var(--spacing-11);
height: var(--spacing-6);
border-radius: 9999px;
background-color: var(--color-neutral-700);
pointer-events: none;
transition: background-color 150ms ease;
}
/* Knob */
.rtds-toggle__lever::before {
content: '';
position: absolute;
top: 50%;
left: var(--spacing-0-5);
transform: translateY(-50%);
width: var(--spacing-5);
height: var(--spacing-5);
border-radius: 9999px;
background-color: var(--color-neutral-50);
transition: left 150ms ease;
z-index: 1;
}
/* Icon inside knob via mask — x-mark (off) default */
.rtds-toggle__lever::after {
content: '';
position: absolute;
top: 50%;
/* Center of knob in off state: edge-offset + half-knob-width */
left: calc(var(--spacing-0-5) + var(--spacing-5) / 2);
transform: translate(-50%, -50%);
width: var(--spacing-3-5);
height: var(--spacing-3-5);
background-color: var(--color-content-01, var(--color-neutral-900));
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M5 5L15 15M15 5L5 15'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M5 5L15 15M15 5L5 15'/%3E%3C/svg%3E");
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
pointer-events: none;
transition: left 150ms ease, background-color 150ms ease;
z-index: 2;
}
/* Form: checked */
.rtds-toggle__input:checked ~ .rtds-toggle__lever {
background-color: var(--color-button-primary, var(--color-primary-500));
}
.rtds-toggle__input:checked ~ .rtds-toggle__lever::before {
left: calc(var(--spacing-11) - var(--spacing-5) - var(--spacing-0-5));
}
/* Icon switches to check, moves with knob to right position */
.rtds-toggle__input:checked ~ .rtds-toggle__lever::after {
left: calc(var(--spacing-11) - var(--spacing-5) - var(--spacing-0-5) + var(--spacing-5) / 2);
background-color: var(--color-button-primary, var(--color-primary-500));
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 10L8 14L16 6'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 10L8 14L16 6'/%3E%3C/svg%3E");
}
/* Form: focus ring on lever */
.rtds-toggle__input:focus-visible ~ .rtds-toggle__lever {
outline: 2px solid var(--color-focusring);
outline-offset: 2px;
}
/* Form: disabled */
.rtds-toggle__input:disabled ~ .rtds-toggle__lever {
opacity: 0.4;
}
.rtds-toggle__label:has(.rtds-toggle__input:disabled) {
cursor: not-allowed;
}
/* Form: state labels */
.rtds-toggle__state-on,
.rtds-toggle__state-off {
font-size: var(--fontsize-text-sm);
color: var(--color-content-02, var(--color-neutral-700));
margin-left: var(--spacing-2);
}
.rtds-toggle__input:checked ~ .rtds-toggle__state-off { display: none; }
.rtds-toggle__input:not(:checked) ~ .rtds-toggle__state-on { display: none; }
/* Form: error */
.rtds-toggle__error {
display: block;
color: var(--color-content-warn, var(--color-warn-dark));
font-size: var(--fontsize-text-sm);
margin-top: var(--spacing-1);
}
/* =====================
Button variant
===================== */
.rtds-toggle__switch {
display: inline-flex;
align-items: center;
gap: var(--spacing-3);
background: none;
border: none;
cursor: pointer;
padding: 0;
color: var(--color-content-01, var(--color-neutral-900));
font-size: var(--fontsize-text-base);
line-height: 1.5;
}
/* Button: checked */
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__lever {
background-color: var(--color-button-primary, var(--color-primary-500));
}
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__lever::before {
left: calc(var(--spacing-11) - var(--spacing-5) - var(--spacing-0-5));
}
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__lever::after {
left: calc(var(--spacing-11) - var(--spacing-5) - var(--spacing-0-5) + var(--spacing-5) / 2);
background-color: var(--color-button-primary, var(--color-primary-500));
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 10L8 14L16 6'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 10L8 14L16 6'/%3E%3C/svg%3E");
}
/* Button: focus ring on lever */
.rtds-toggle__switch:focus-visible .rtds-toggle__lever {
outline: 2px solid var(--color-focusring);
outline-offset: 2px;
}
/* Button: disabled */
.rtds-toggle__switch:disabled {
cursor: not-allowed;
opacity: 0.4;
}
/* Button: state labels */
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__state-off { display: none; }
.rtds-toggle__switch[aria-checked="false"] .rtds-toggle__state-on { display: none; }
@layer components {
/* Wrapper */
.rtds-toggle {
display: block;
}
/* =====================
Form variant
===================== */
.rtds-toggle__label {
display: inline-flex;
align-items: center;
gap: var(--spacing-3);
cursor: pointer;
}
.rtds-toggle__text {
font-size: var(--fontsize-text-base);
color: var(--color-content-01, var(--color-neutral-900));
line-height: 1.5;
}
.rtds-toggle__text abbr {
text-decoration: none;
}
.rtds-toggle__control {
position: relative;
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
/* Visually hidden but accessible and focusable — label wraps the control so clicks on __lever activate the input */
.rtds-toggle__input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Visual track */
.rtds-toggle__lever {
display: inline-block;
position: relative;
flex-shrink: 0;
width: var(--spacing-11);
height: var(--spacing-6);
border-radius: 9999px;
background-color: var(--color-neutral-700);
pointer-events: none;
transition: background-color 150ms ease;
}
/* Knob */
.rtds-toggle__lever::before {
content: '';
position: absolute;
top: 50%;
left: var(--spacing-0-5);
transform: translateY(-50%);
width: var(--spacing-5);
height: var(--spacing-5);
border-radius: 9999px;
background-color: var(--color-neutral-50);
transition: left 150ms ease;
z-index: 1;
}
/* Icon inside knob via mask — x-mark (off) default */
.rtds-toggle__lever::after {
content: '';
position: absolute;
top: 50%;
/* Center of knob in off state: edge-offset + half-knob-width */
left: calc(var(--spacing-0-5) + var(--spacing-5) / 2);
transform: translate(-50%, -50%);
width: var(--spacing-3-5);
height: var(--spacing-3-5);
background-color: var(--color-content-01, var(--color-neutral-900));
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M5 5L15 15M15 5L5 15'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M5 5L15 15M15 5L5 15'/%3E%3C/svg%3E");
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
pointer-events: none;
transition: left 150ms ease, background-color 150ms ease;
z-index: 2;
}
/* Form: checked */
.rtds-toggle__input:checked ~ .rtds-toggle__lever {
background-color: var(--color-button-primary, var(--color-primary-500));
}
.rtds-toggle__input:checked ~ .rtds-toggle__lever::before {
left: calc(var(--spacing-11) - var(--spacing-5) - var(--spacing-0-5));
}
/* Icon switches to check, moves with knob to right position */
.rtds-toggle__input:checked ~ .rtds-toggle__lever::after {
left: calc(var(--spacing-11) - var(--spacing-5) - var(--spacing-0-5) + var(--spacing-5) / 2);
background-color: var(--color-button-primary, var(--color-primary-500));
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 10L8 14L16 6'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 10L8 14L16 6'/%3E%3C/svg%3E");
}
/* Form: focus ring on lever */
.rtds-toggle__input:focus-visible ~ .rtds-toggle__lever {
outline: 2px solid var(--color-focusring);
outline-offset: 2px;
}
/* Form: disabled */
.rtds-toggle__input:disabled ~ .rtds-toggle__lever {
opacity: 0.4;
}
.rtds-toggle__label:has(.rtds-toggle__input:disabled) {
cursor: not-allowed;
}
/* Form: state labels */
.rtds-toggle__state-on,
.rtds-toggle__state-off {
font-size: var(--fontsize-text-sm);
color: var(--color-content-02, var(--color-neutral-700));
margin-left: var(--spacing-2);
}
.rtds-toggle__input:checked ~ .rtds-toggle__state-off { display: none; }
.rtds-toggle__input:not(:checked) ~ .rtds-toggle__state-on { display: none; }
/* Form: error */
.rtds-toggle__error {
display: block;
color: var(--color-content-warn, var(--color-warn-dark));
font-size: var(--fontsize-text-sm);
margin-top: var(--spacing-1);
}
/* =====================
Button variant
===================== */
.rtds-toggle__switch {
display: inline-flex;
align-items: center;
gap: var(--spacing-3);
background: none;
border: none;
cursor: pointer;
padding: 0;
color: var(--color-content-01, var(--color-neutral-900));
font-size: var(--fontsize-text-base);
line-height: 1.5;
}
/* Button: checked */
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__lever {
background-color: var(--color-button-primary, var(--color-primary-500));
}
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__lever::before {
left: calc(var(--spacing-11) - var(--spacing-5) - var(--spacing-0-5));
}
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__lever::after {
left: calc(var(--spacing-11) - var(--spacing-5) - var(--spacing-0-5) + var(--spacing-5) / 2);
background-color: var(--color-button-primary, var(--color-primary-500));
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 10L8 14L16 6'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 10L8 14L16 6'/%3E%3C/svg%3E");
}
/* Button: focus ring on lever */
.rtds-toggle__switch:focus-visible .rtds-toggle__lever {
outline: 2px solid var(--color-focusring);
outline-offset: 2px;
}
/* Button: disabled */
.rtds-toggle__switch:disabled {
cursor: not-allowed;
opacity: 0.4;
}
/* Button: state labels */
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__state-off { display: none; }
.rtds-toggle__switch[aria-checked="false"] .rtds-toggle__state-on { display: none; }
}
Interruttore binario (on/off) con due varianti semanticamente distinte: form per contesti form, button per azioni dirette.
toggle.njk)Renderizza <div.rtds-toggle> > <label> > <span.__text> + <span.__control>. All’interno di __control: <input type="checkbox" role="switch"> (elemento interattivo) + <span.__lever> (rappresentazione visiva). Lo stato checked è gestito nativamente dal browser e viene inviato con il form.
toggle--button.njk)Renderizza <div.rtds-toggle> > <button role="switch" aria-checked> contenente <span.__text> + <span.__lever>. Il nome accessibile è fornito dal testo interno __text. Il consumer JS è responsabile di aggiornare aria-checked ad ogni click.
Nota per il consumer: senza JavaScript, la variante button non comunica il cambio di stato alle AT. Aggiornare
aria-checkedad ogni click:btn.setAttribute('aria-checked', btn.getAttribute('aria-checked') === 'true' ? 'false' : 'true').
| Variante | Template | Stato |
|---|---|---|
form |
toggle.njk |
off (default) |
form-checked |
toggle.njk |
on |
form-disabled |
toggle.njk |
disabilitato |
form-disabled-checked |
toggle.njk |
disabilitato + on |
form-required |
toggle.njk |
obbligatorio |
form-has-error |
toggle.njk |
errore validazione |
form-with-state-labels |
toggle.njk |
label On/Off visibili |
button |
toggle--button.njk |
off (default) |
button-checked |
toggle--button-checked.njk |
on |
button-disabled |
toggle--button.njk |
disabilitato |
button-with-state-labels |
toggle--button.njk |
label On/Off visibili |
| Parametro | Tipo | Obbligatorio | Default | Descrizione |
|---|---|---|---|---|
id |
stringa | sì | — | ID univoco del controllo; deve essere unico in pagina |
label |
stringa | sì | — | Testo etichetta visibile. Supporta HTML inline (| safe) |
checked |
booleano | no | false |
Stato iniziale on |
disabled |
booleano | no | false |
Disabilita il toggle |
classes |
stringa | no | — | Classi CSS aggiuntive sul wrapper .rtds-toggle |
showStateLabels |
booleano | no | false |
Mostra testo “On”/“Off” accanto al lever |
onLabel |
stringa | no | 'On' |
Testo stato attivo — usato solo se showStateLabels: true |
offLabel |
stringa | no | 'Off' |
Testo stato inattivo — usato solo se showStateLabels: true |
ariaDescribedBy |
stringa | no | — | ID di un elemento esterno di descrizione aggiuntiva |
| Parametro | Tipo | Obbligatorio | Default | Descrizione |
|---|---|---|---|---|
name |
stringa | no | valore di id |
Attributo name dell’input per la submission del form |
fieldRequired |
booleano | no | false |
Aggiunge aria-required="true" e asterisco * visibile nella label |
hasError |
booleano | no | false |
Aggiunge .has-error al wrapper |
ariaInvalid |
booleano | no | false |
Aggiunge aria-invalid="true" sull’input |
hasErrorControl |
booleano | no | false |
Mostra .rtds-toggle__error; collega automaticamente via aria-describedby |
errorDescription |
stringa | no | — | Testo del messaggio di errore inline |
| Criterio | Come soddisfatto |
|---|---|
| 1.3.1 Informazioni e correlazioni | Form: <label for> associa testo e input. Button: nome accessibile da text content interno (__text) |
| 2.1.1 Tastiera | Form: <input> nativo attivabile con Space. Button: <button> nativo attivabile con Space ed Enter |
| 4.1.2 Nome, Ruolo, Valore | role="switch" + stato comunicato da checked (form) / aria-checked aggiornato dal consumer (button) |
| 1.4.11 Contrasto in contenuti non testuali | Track unchecked (--color-neutral-700, ~4.3:1 su bianco), track checked (--color-button-primary, >3:1), focus ring >3:1. Stato disabled (opacity 0.4) escluso dal criterio |
| Tasto | Azione |
|---|---|
Tab / Shift+Tab |
Sposta il focus sul/dal toggle |
Space |
Attiva/disattiva (entrambe le varianti) |
Enter |
Attiva/disattiva (solo button variant) |
showStateLabels) hanno aria-hidden="true" — lo stato è già comunicato dal controllo.rtds-toggle__error con aria-live="assertive" annuncia il messaggio appena compare| Classe | Elemento | Variante |
|---|---|---|
.rtds-toggle |
wrapper <div> |
entrambe |
.rtds-toggle.has-error |
modificatore errore | solo form |
.rtds-toggle__label |
<label> |
solo form |
.rtds-toggle__text |
<span> testo etichetta |
entrambe |
.rtds-toggle__control |
<span> contenitore input+lever |
solo form |
.rtds-toggle__input |
<input type="checkbox"> |
solo form |
.rtds-toggle__switch |
<button role="switch"> |
solo button |
.rtds-toggle__lever |
<span> rappresentazione visiva (track+knob) |
entrambe |
.rtds-toggle__state-on |
<span> testo “On” |
entrambe (se showStateLabels) |
.rtds-toggle__state-off |
<span> testo “Off” |
entrambe (se showStateLabels) |
.rtds-toggle__error |
<span> messaggio errore |
solo form |
__lever| Pseudo-elemento | Ruolo |
|---|---|
::before |
Knob (cerchio bianco mobile) |
::after |
Icona dentro il knob — × (off, --color-content-01) o ✓ (on, --color-button-primary) via mask-image. Path stroke stroke-width="2.5", dimensione --spacing-3-5 |
L’__input è nascosto con pattern sr-only (position: absolute; clip; 1px×1px) ma rimane focusable. Il clic sul __lever attiva l’input perché il <label> avvolge tutta la struttura.
/* Form — checked: track, knob, icona */
.rtds-toggle__input:checked ~ .rtds-toggle__lever { background-color: ...; }
.rtds-toggle__input:checked ~ .rtds-toggle__lever::before { left: ...; }
.rtds-toggle__input:checked ~ .rtds-toggle__lever::after { /* icona check, colore primario */ }
/* Form — focus ring sul lever */
.rtds-toggle__input:focus-visible ~ .rtds-toggle__lever { outline: ...; }
/* Form — disabled */
.rtds-toggle__input:disabled ~ .rtds-toggle__lever { opacity: 0.4; }
/* Button — checked */
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__lever { ... }
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__lever::before { left: ...; }
.rtds-toggle__switch[aria-checked="true"] .rtds-toggle__lever::after { /* icona check */ }
/* Button — focus */
.rtds-toggle__switch:focus-visible .rtds-toggle__lever { outline: ...; }<form>, button se l’azione è diretta (es. tema, preferenza UI non legata a un form)aria-checked le AT non percepiscono il cambio di statoid deve essere univoco in pagina per garantire la corretta associazione label/controllo