Tucano v0.37.2

Caixa, opção e chave

Os três são o <input> nativo com o desenho trocado, e nada mais. O name, o value, o required e o anúncio do leitor de tela continuam sendo os do navegador — o formulário posta como sempre postou, e não há JavaScript nenhum.

Caixa de seleção
Entrega
Chave

Exemplos

Tudo abaixo é o mesmo <input> com classes e atributos diferentes.

Estado misto

O indeterminate nativo vira um traço. Ele não existe como atributo de HTML — só como propriedade, em JavaScript.

Lado a lado

is-inline no grupo põe as opções na mesma linha, quebrando quando não cabem.

Periodicidade

Com erro

aria-invalid="true" no input pinta a borda enquanto ele está desmarcado — o Django 5 já escreve o atributo.

Aceite os termos para continuar.

Controle à direita

is-end faz a linha inteira clicável, com os controles alinhados numa coluna — a lista de preferências.

Como usar

Classe no input e um <label class="tuc-choice"> em volta. Clicar no texto marca, e o leitor de tela lê o texto como nome do controle — sem for nem id.

<label class="tuc-choice">
  <input type="checkbox" class="tuc-check" name="invoice"> Nota fiscal por e-mail
</label>

<fieldset class="tuc-choices">
  <legend>Entrega</legend>
  <label class="tuc-choice">
    <input type="radio" class="tuc-radio" name="shipping" value="standard" checked>
    <span>Normal <span class="tuc-choice__hint">Até 7 dias úteis</span></span>
  </label>
  <label class="tuc-choice">
    <input type="radio" class="tuc-radio" name="shipping" value="express">
    <span>Expressa <span class="tuc-choice__hint">Chega amanhã</span></span>
  </label>
</fieldset>

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

Com descrição, texto e .tuc-choice__hint vão dentro de um <span>: é o que mantém os dois empilhados ao lado do controle, alinhado à primeira linha.

No formulário do Django

A classe vai nos attrs do widget. Para opções (RadioSelect, CheckboxSelectMultiple), percorra o campo no template: cada item entrega o input pronto em tag e o texto em choice_label.

class PreferencesForm(forms.Form):
    invoice = forms.BooleanField(
        required=False, label="Nota fiscal por e-mail",
        widget=forms.CheckboxInput(attrs={"class": "tuc-check"}),
    )
    notifications = forms.BooleanField(
        required=False, label="Avisos por e-mail",
        widget=forms.CheckboxInput(attrs={"class": "tuc-switch", "role": "switch"}),
    )
    shipping = forms.ChoiceField(
        choices=[("standard", "Normal"), ("express", "Expressa")],
        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>

O POST é o do HTML: caixa marcada envia o value (ou on, sem ele), desmarcada não envia nada — é por isso que o BooleanField precisa de required=False.

Estado misto

Para a caixa que resume um grupo, como o "selecionar todos" de uma tabela. Só existe como propriedade:

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

Chave é para o que acontece na hora

Caixa, para o que só vale ao salvar. Uma chave num formulário que ainda pede "Salvar" promete um efeito que não aconteceu.

Por que desenhados

accent-color resolveria em uma linha, mas entrega o desenho do sistema: no macOS um azul que não é o seu, no Windows outro canto, e nenhum dos dois com o raio do resto dos campos.

E ele só pinta a caixa marcada — a opção ao lado continuaria com o círculo do sistema. Desenhados, os três combinam entre si e com o input vizinho em qualquer sistema, e usam os mesmos tokens: o destaque é --tuc-accent, o anel de foco é o --tuc-accent-ring dos campos.

Resiste ao CSS do projeto

As classes são repetidas no seletor (input.tuc-check.tuc-check) para vencer regras comuns como input[type=checkbox] { ... } e .card label { display: block }, que com uma classe só desmontariam a linha.

Teclado e acessibilidade

Tudo é do navegador, porque o controle continua sendo o input. O <fieldset> com <legend> dá ao leitor de tela o nome do grupo de opções.

TeclaAção
TabChega na caixa ou na chave; num grupo de opções, na que está marcada
EspaçoMarca e desmarca a caixa, liga e desliga a chave
↑ ↓ ← →Anda entre as opções de mesmo name, marcando

A chave precisa de role="switch" no template

CSS não põe papel. É o role que faz o leitor de tela dizer "chave, ligada" em vez de "caixa de seleção, marcada" — e quem esquecer continua com um checkbox que funciona, só anunciado pelo nome errado.

Com prefers-reduced-motion as transições de marcar e de deslizar a chave são desligadas.

Classes

Só CSS. Não há componente para inicializar nem evento próprio: ouça o change nativo.

Classe ou atributoOndePara quê
tuc-checkinput type="checkbox"Caixa de seleção; o estado misto é o indeterminate nativo
tuc-radioinput type="radio"Opção — uma de várias com o mesmo name
tuc-switchinput type="checkbox"Chave; exige role="switch" no input
tuc-choicelabelJunta controle e texto, com o controle alinhado à primeira linha
tuc-choice__hintspan dentro do textoDescrição embaixo do texto
is-end.tuc-choiceControle à direita e a linha inteira clicável
tuc-choicesfieldset ou divGrupo empilhado; desfaz a borda, o padding e o min-width do fieldset
is-inline.tuc-choicesOpções lado a lado, quebrando linha
disabledinputEsmaece o controle e o texto do rótulo, com cursor de bloqueio
aria-invalid="true"inputBorda de erro enquanto desmarcado; :user-invalid faz o mesmo depois de a pessoa mexer