Tucano v0.37.2

Casilla, opción e interruptor

Los tres son el <input> nativo con otro aspecto, y nada más. El name, el value, el required y el anuncio del lector de pantalla siguen siendo los del navegador — el formulario envía como siempre, y no hay JavaScript alguno.

Casilla de verificación
Envío
Interruptor

Ejemplos

Todo lo de abajo es el mismo <input> con clases y atributos distintos.

Estado mixto

El indeterminate nativo se convierte en un guion. No existe como atributo de HTML — solo como propiedad, en JavaScript.

Lado a lado

is-inline en el grupo pone las opciones en la misma línea, saltando de línea cuando no caben.

Periodicidad

Con error

aria-invalid="true" en el input pinta el borde mientras está desmarcado — Django 5 ya escribe el atributo.

Acepta los términos para continuar.

Control a la derecha

is-end hace clicable toda la fila, con los controles alineados en una columna — la lista de preferencias.

Cómo usar

Una clase en el input y un <label class="tuc-choice"> alrededor. Hacer clic en el texto marca, y el lector de pantalla lee el texto como nombre del control — sin for ni id.

<label class="tuc-choice">
  <input type="checkbox" class="tuc-check" name="invoice"> Factura por correo
</label>

<fieldset class="tuc-choices">
  <legend>Envío</legend>
  <label class="tuc-choice">
    <input type="radio" class="tuc-radio" name="shipping" value="standard" checked>
    <span>Normal <span class="tuc-choice__hint">Hasta 7 días hábiles</span></span>
  </label>
  <label class="tuc-choice">
    <input type="radio" class="tuc-radio" name="shipping" value="express">
    <span>Exprés <span class="tuc-choice__hint">Llega mañana</span></span>
  </label>
</fieldset>

<label class="tuc-choice is-end">
  <input type="checkbox" role="switch" class="tuc-switch" name="notifications"> Avisos por correo
</label>

Con descripción, el texto y .tuc-choice__hint van dentro de un <span>: es lo que mantiene los dos apilados junto al control, alineado con la primera línea.

En el formulario de Django

La clase va en los attrs del widget. Para opciones (RadioSelect, CheckboxSelectMultiple), recorre el campo en el template: cada ítem entrega el input listo en tag y el texto en choice_label.

class PreferencesForm(forms.Form):
    invoice = forms.BooleanField(
        required=False, label="Factura por correo",
        widget=forms.CheckboxInput(attrs={"class": "tuc-check"}),
    )
    notifications = forms.BooleanField(
        required=False, label="Avisos por correo",
        widget=forms.CheckboxInput(attrs={"class": "tuc-switch", "role": "switch"}),
    )
    shipping = forms.ChoiceField(
        choices=[("standard", "Normal"), ("express", "Exprés")],
        widget=forms.RadioSelect(attrs={"class": "tuc-radio"}),
    )
<label class="tuc-choice">{{ form.invoice }} {{ form.invoice.label }}</label>

<label class="tuc-choice is-end">{{ form.notifications }} <span>{{ form.notifications.label }}</span></label>

<fieldset class="tuc-choices">
  <legend>{{ form.shipping.label }}</legend>
  {% for option in form.shipping %}
    <label class="tuc-choice">{{ option.tag }} {{ option.choice_label }}</label>
  {% endfor %}
</fieldset>

El POST es el de HTML: una casilla marcada envía su value (u on, si no tiene), una desmarcada no envía nada — por eso el BooleanField necesita required=False.

Estado mixto

Para la casilla que resume un grupo, como el "seleccionar todo" de una tabla. Solo existe como propiedad:

document.querySelector('#all').indeterminate = true;

El interruptor es para lo que ocurre al instante

La casilla, para lo que solo vale al guardar. Un interruptor en un formulario que todavía pide "Guardar" promete un efecto que no ocurrió.

Por qué dibujados

accent-color lo resolvería en una línea, pero entrega el aspecto del sistema: en macOS un azul que no es el tuyo, en Windows otras esquinas, y ninguno con el radio del resto de los campos.

Y solo pinta la casilla marcada — la opción de al lado seguiría con el círculo del sistema. Dibujados, los tres combinan entre sí y con el input vecino en cualquier sistema, y usan los mismos tokens: el acento es --tuc-accent, el anillo de foco es el --tuc-accent-ring de los campos.

Resiste el CSS del proyecto

Las clases se repiten en el selector (input.tuc-check.tuc-check) para ganarle a reglas comunes como input[type=checkbox] { ... } y .card label { display: block }, que con una sola clase desarmarían la fila.

Teclado y accesibilidad

Todo es del navegador, porque el control sigue siendo el input. El <fieldset> con <legend> le da al lector de pantalla el nombre del grupo de opciones.

TeclaAcción
TabLlega a la casilla o al interruptor; en un grupo de opciones, a la marcada
EspacioMarca y desmarca la casilla, enciende y apaga el interruptor
↑ ↓ ← →Recorre las opciones con el mismo name, marcándolas

El interruptor necesita role="switch" en el template

El CSS no asigna roles. Es el role lo que hace que el lector de pantalla diga "interruptor, activado" en lugar de "casilla de verificación, marcada" — y quien lo olvide sigue teniendo un checkbox que funciona, solo anunciado con el nombre equivocado.

Con prefers-reduced-motion se desactivan las transiciones al marcar y al deslizar el interruptor.

Clases

Solo CSS. No hay componente que inicializar ni evento propio: escucha el change nativo.

Clase o atributoDóndePara qué
tuc-checkinput type="checkbox"Casilla de verificación; el estado mixto es el indeterminate nativo
tuc-radioinput type="radio"Opción — una de varias con el mismo name
tuc-switchinput type="checkbox"Interruptor; exige role="switch" en el input
tuc-choicelabelUne control y texto, con el control alineado a la primera línea
tuc-choice__hintspan dentro del textoDescripción debajo del texto
is-end.tuc-choiceControl a la derecha y toda la fila clicable
tuc-choicesfieldset o divGrupo apilado; quita el borde, el padding y el min-width del fieldset
is-inline.tuc-choicesOpciones lado a lado, saltando de línea
disabledinputAtenúa el control y el texto de la etiqueta, con cursor de bloqueo
aria-invalid="true"inputBorde de error mientras está desmarcado; :user-invalid hace lo mismo después de que la persona interactúa