Color picker
Un campo de color con área de saturación y brillo, tono, opacidad, paleta y cuentagotas. El
<input type="text"> sigue guardando el valor y el name, así que el formulario envía
#4f46e5 y el hex se puede seguir escribiendo si ya sabes el color.
Ejemplos
El mismo <input> con atributos distintos. El panel se abre desde la muestra a la izquierda del campo.
Sin valor
Un campo sin value nace vacío, sin ningún color elegido por ti, y required bloquea
el envío hasta que alguien elija uno.
Sin opacidad
data-alpha="false" quita la barra; el hex siempre tiene 6 dígitos.
Formato hsl
data-format="hsl"; con opacidad menor que 1, sale hsla().
Paleta propia
data-swatches con los colores de la marca, separados por comas.
Sin paleta
data-swatches="false" deja solo el área, las barras y el valor.
Panel alineado a la izquierda
data-placement="bottom-start". Los bordes de la pantalla siguen mandando.
Con error
aria-invalid="true" en el campo — Django 5 ya lo escribe.
Otra etiqueta ya usa este color.
Cómo usar
Marca el <input> y se inicializa solo al cargar la página y en cada htmx:afterSwap.
La muestra y el valor se convierten en un único control, .tuc-color-field, con la altura, el radio y el anillo de foco del 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 | Por defecto | Para qué |
|---|---|---|
data-format | hex | Formato del valor: hex, rgb o hsl |
data-alpha | true | false quita la barra de opacidad |
data-swatches | paleta de 15 colores | Colores separados por comas, o false para ocultarla |
data-placement | bottom-center | Lado y alineación del panel |
En el formulario de Django
class TagForm(forms.ModelForm):
class Meta:
model = Tag
fields = ["name", "color"]
widgets = {
"color": forms.TextInput(attrs={"data-tuc-color": "", "data-alpha": "false"}),
}Un CharField(max_length=9) alcanza para el hex con opacidad (#rrggbbaa). Usa TextInput, y no
type="color": el nativo no tiene opacidad y cambiaría el aspecto del campo.
En 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; un texto que no es color devuelve false y no cambia nada
c.open();
c.destroy();Valor y formulario
Quien envía es tu propio <input>, con su name. El texto queda en el formato pedido,
sea cual sea la notación usada para elegir.
| Formato | Opaco | Con opacidad |
|---|---|---|
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) |
Como entrada, escrita en el campo o en el panel, valen #rgb, #rgba, #rrggbb, #rrggbbaa,
rgb(), rgba(), hsl() y hsla(). El texto se lee al confirmar — salir del campo o
Enter — y un texto que no es color vuelve al valor actual, en lugar de borrar el color.
Cada cambio dispara tucano:change en el campo, y también el change nativo, para la validación y HTMX.
Al arrastrar, como en el <input type="range">, el tucano:change sale en cada movimiento y el
change nativo una sola vez, al 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; // el ColorPicker
});Para elegir texto claro u oscuro sobre el color que la persona eligió, Tucano.color.isDark('#4f46e5') devuelve
true — mide la luminancia, y no el promedio de los canales, que no es como lo lee el ojo.
El estado guardado es HSVA, y no RGB
Convertir a RGB en cada movimiento pierde el tono cuando la saturación llega a cero: todo gris se volvería rojo al aclararlo de nuevo. Guardando tono, saturación, brillo y opacidad, arrastrar hasta el blanco y volver devuelve el color del que se partió.
Cuentagotas solo donde el navegador lo tiene
El botón para capturar un color de la pantalla aparece cuando existe la API EyeDropper — hoy, Chrome y Edge en escritorio. En los demás simplemente no se dibuja, en lugar de quedarse ahí sin funcionar.
Teclado y accesibilidad
El disparador es la muestra junto al campo, un <button> de verdad, con
aria-haspopup="dialog" y aria-expanded. Llegar con Tab no abre nada.
| Tecla | Dónde | Acción |
|---|---|---|
Enter Espacio | Muestra | Abre el panel con el foco en el área, y lo cierra |
↓ | Muestra o campo | Abre el panel con el foco en el área |
← → | Área | Saturación, de 2 en 2%; con Shift, de 10 en 10 |
↑ ↓ | Área | Brillo, con el mismo paso |
← → ↑ ↓ | Barras | Tono de 1 en 1 grado, opacidad de 1 en 1%; con Shift, de 10 en 10 |
Home End | Barras | Mínimo y máximo |
Esc | Panel | Cierra y devuelve el foco a la muestra |
Por qué el foco en el campo no abre el panel
Abrir con el foco molestaba dos veces: el panel aparecía solo con tabular por el formulario, y tapaba el propio campo a quien quería escribir el hex. Por eso el disparador es la muestra, que ya responde a Enter y Espacio por ser un botón, y en el campo queda solo la flecha hacia abajo, la misma del date picker.
Las barras son role="slider" con aria-valuenow, el área tiene la etiqueta "Saturação e brilho" (saturación y brillo — las etiquetas por defecto están en portugués, y Tucano.setTexts({ colorpicker }) las reemplaza), y el valor en el
panel es un .tuc-input común. El panel se cierra cuando el foco sale de él o con un clic afuera; con
prefers-reduced-motion, solo se desvanece.
API
Generada a partir del código en cada build — si algo no está aquí, no existe.
[data-tuc-color]new Tucano.ColorPicker(alvo, opcoes)data-alpha data-format data-placement data-swatchesgetValue getRgb setValue open close toggle destroytucano:changeOpciones
Las notas de esta tabla salen de los comentarios del código fuente, que están escritos en portugués.
| Opción | Por defecto | 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 |