Color picker
Um campo de cor com área de saturação e brilho, matiz, opacidade, paleta e conta-gotas. O
<input type="text"> continua guardando o valor e o name, então o formulário posta
#4f46e5 e o hex continua digitável para quem já sabe a cor.
Exemplos
O mesmo <input> com atributos diferentes. O painel abre pela amostra à esquerda do campo.
Sem valor
Campo sem value nasce vazio, sem cor escolhida por você, e o required barra o envio
até alguém escolher.
Sem opacidade
data-alpha="false" tira a trilha; o hex fica sempre com 6 dígitos.
Formato hsl
data-format="hsl"; com opacidade abaixo de 1, sai hsla().
Paleta própria
data-swatches com as cores da marca, separadas por vírgula.
Sem paleta
data-swatches="false" deixa só a área, as trilhas e o valor.
Painel alinhado à esquerda
data-placement="bottom-start". As bordas da tela continuam mandando.
Com erro
aria-invalid="true" no campo — o Django 5 já escreve.
Essa cor já está em uso por outra etiqueta.
Como usar
Marque o <input> e ele inicializa sozinho no carregamento e a cada htmx:afterSwap.
A amostra e o valor viram um controle só, .tuc-color-field, com a altura, o raio e o anel de foco do Select.
<input type="text" name="color" value="#4f46e5" data-tuc-color>
<input type="text" name="brand" value="#0d9488" data-tuc-color data-alpha="false"
data-swatches="#0a0a0a,#ea580c,#16a34a">| Atributo | Padrão | Para quê |
|---|---|---|
data-format | hex | Formato do valor: hex, rgb ou hsl |
data-alpha | true | false tira a trilha de opacidade |
data-swatches | paleta de 15 cores | Cores separadas por vírgula, ou false para esconder |
data-placement | bottom-center | Lado e alinhamento do painel |
No formulário do Django
class TagForm(forms.ModelForm):
class Meta:
model = Tag
fields = ["name", "color"]
widgets = {
"color": forms.TextInput(attrs={"data-tuc-color": "", "data-alpha": "false"}),
}Um CharField(max_length=9) cabe o hex com opacidade (#rrggbbaa). Use TextInput, e não
type="color": o nativo não tem opacidade e mudaria o desenho do campo.
Em JavaScript
const c = new Tucano.ColorPicker('#color', {
format: 'rgb',
alpha: false,
swatches: ['#0a0a0a', '#ea580c', '#16a34a'],
onChange: (value, { rgb, hsva }) => console.log(value),
});
c.getValue(); // 'rgb(79, 70, 229)'
c.getRgb(); // { r: 79, g: 70, b: 229, a: 1 }
c.setValue('#16a34a'); // true; texto que não é cor devolve false e nada muda
c.open();
c.destroy();Valor e formulário
Quem posta é o seu <input>, com o name dele. O texto fica no formato pedido,
qualquer que seja a notação usada para escolher.
| Formato | Opaca | Com opacidade |
|---|---|---|
hex | #4f46e5 | #4f46e599 |
rgb | rgb(79, 70, 229) | rgba(79, 70, 229, 0.6) |
hsl | hsl(243, 75%, 59%) | hsla(243, 75%, 59%, 0.6) |
Na entrada, digitada no campo ou no painel, valem #rgb, #rgba, #rrggbb, #rrggbbaa,
rgb(), rgba(), hsl() e hsla(). O texto é lido ao confirmar — sair do campo ou
Enter — e texto que não é cor volta para o valor atual, em vez de zerar a cor.
Cada mudança dispara tucano:change no campo, e também o change nativo, para validação e HTMX.
No arrasto, como no <input type="range">, o tucano:change sai a cada movimento e o
change nativo uma vez só, ao soltar.
document.querySelector('#color').addEventListener('tucano:change', (e) => {
e.detail.value; // '#4f46e5'
e.detail.rgb; // { r, g, b, a }
e.detail.hsva; // { h, s, v, a }
e.detail.instance; // o ColorPicker
});Para escolher texto claro ou escuro sobre a cor que a pessoa escolheu, Tucano.color.isDark('#4f46e5') devolve
true — ele mede a luminância, e não a média dos canais, que o olho não lê assim.
O estado guardado é HSVA, e não RGB
Converter para RGB a cada movimento perde a matiz quando a saturação chega a zero: todo cinza viraria vermelho ao clarear de novo. Guardando matiz, saturação, brilho e opacidade, arrastar até o branco e voltar devolve a cor de onde se saiu.
Conta-gotas só onde o navegador tem
O botão de capturar cor da tela aparece quando existe a API EyeDropper — hoje, Chrome e Edge no desktop. Nos outros ele simplesmente não é desenhado, em vez de ficar lá sem funcionar.
Teclado e acessibilidade
O gatilho é a amostra ao lado do campo, um <button> de verdade, com
aria-haspopup="dialog" e aria-expanded. Chegar de Tab não abre nada.
| Tecla | Onde | Ação |
|---|---|---|
Enter Espaço | Amostra | Abre o painel com o foco na área, e fecha |
↓ | Amostra ou campo | Abre o painel com o foco na área |
← → | Área | Saturação, de 2 em 2%; com Shift, de 10 em 10 |
↑ ↓ | Área | Brilho, no mesmo passo |
← → ↑ ↓ | Trilhas | Matiz de 1 em 1 grau, opacidade de 1 em 1%; com Shift, de 10 em 10 |
Home End | Trilhas | Mínimo e máximo |
Esc | Painel | Fecha e devolve o foco à amostra |
Por que o foco no campo não abre o painel
Abrir no foco atrapalhava duas vezes: o painel subia só de tabular pelo formulário, e cobria o próprio campo de quem queria digitar o hex. Por isso o gatilho é a amostra, que já responde a Enter e Espaço por ser botão, e no campo fica só a seta para baixo, a mesma do date picker.
As trilhas são role="slider" com aria-valuenow, a área tem rótulo "Saturação e brilho", e o valor no
painel é um .tuc-input comum. O painel fecha quando o foco sai dele ou com clique fora; com
prefers-reduced-motion, só esmaece.
API
Gerada do código a cada build — se algo não está aqui, não existe.
[data-tuc-color]new Tucano.ColorPicker(alvo, opcoes)data-alpha data-format data-placement data-swatchesgetValue getRgb setValue open close toggle destroytucano:changeOpções
| Opção | Padrão | Para quê |
|---|---|---|
format | 'hex' | 'hex' | 'rgb' | 'hsl' |
alpha | true | |
swatches | PALETTE | false desliga |
placement | 'bottom-center' | mesma regra do date picker: centralizado, preso na borda da tela |
appendTo | undefined | |
onChange | null |