Saltar al contenido

CSS, de a poco

Curso de CSS

Formularios

Lección 27 de 37 · 5 min

Los controles nativos ya traen teclado, foco y lectores de pantalla resueltos. Con unas pocas propiedades se ven como parte de tu diseño, avisan los errores en el momento justo y se adaptan a lo que escribe cada persona.

Color de acento y del cursor

accent-color le da un color a las partes marcadas de los controles nativos: el tilde del checkbox, el punto del radio, el relleno del range y la barra de progress. El navegador elige solo el color del tilde para que contraste. caret-color pinta el cursor que parpadea en los campos de texto.

Las dos se heredan, así que alcanza con ponerlas una vez en el formulario o en :root.

form.css
form {
accent-color: oklch(0.56 0.13 155);
caret-color: oklch(0.56 0.13 155);
}

caret-color es Baseline desde hace años. accent-color todavía no figura como Baseline: está desde Chrome 93 y Firefox 92, y Safari lo soporta del todo recién desde la 26.2 (antes, en parte). Donde no llega, el control queda con su color de siempre, así que se puede usar sin miedo.

Un detalle: si le ponés borde o fondo propio a un control, algunos navegadores (Chrome, por ejemplo) dejan de dibujarlo nativo y accent-color ya no se aplica. Un reset global de bordes, como el border: 0 solid que trae Tailwind, alcanza para que un progress se vuelva una barra verde plana; border: revert en ese control lo devuelve.

Rediseñar un control

Cuando un color no alcanza, appearance: none le saca al control el dibujo del sistema y lo deja como una caja vacía que estilás como cualquier otra. Sigue siendo un input de verdad: se marca con Espacio, se envía con el formulario y lo anuncian los lectores de pantalla. El tilde se dibuja con un ::before y :checked cambia el estado.

Cambiá el color y activá el checkbox dibujado: el resto de los controles sigue leyendo accent-color, y el checkbox nuevo usa el mismo valor.

accent-color: oklch(0.56 0.13 155)checkbox: nativo

Finca El Mirador

Huila, Colombia · tueste medio

Molienda
500 g
accent-color
pedido.css
.pedido {
accent-color: oklch(0.56 0.13 155);
caret-color: oklch(0.56 0.13 155);
}
pedido.html
<form class="accent-[oklch(0.56_0.13_155)] caret-[oklch(0.56_0.13_155)]">
<label><input type="checkbox"> Repetir cada 15 días</label>
…
</form>

Si redibujás un control, el foco queda a tu cargo: sumale un :focus-visible con outline. El botón de los campos de archivo tiene su propio pseudoelemento, ::file-selector-button, Baseline desde 2021:

archivo.css
input[type="file"]::file-selector-button {
border: 0;
border-radius: 6px;
padding: 6px 12px;
background: oklch(0.95 0 0);
font: inherit;
}

Errores en el momento justo

:invalid coincide con cualquier campo que no pasa la validación del HTML (required, type="email", pattern). El problema es que coincide desde que carga la página: un formulario vacío aparece lleno de errores antes de que escribas nada.

El parche clásico fue sumar :not(:placeholder-shown): mientras se ve el placeholder, el campo está vacío y no se marca. Pero apenas escribís la primera letra de un email ya es inválido, y se pone rojo mientras todavía estás escribiendo.

:user-invalid resuelve eso: coincide recién cuando la persona cambió el campo y salió de él, o cuando intentó enviar el formulario. Su par es :user-valid. Las dos son Baseline ampliamente disponible.

El mismo campo de email en tres momentos: recién cargado, escribiendo «ana@» y después de salir del campo. Con :invalid se pinta como error en los tres. Con :not(:placeholder-shown) se salva al cargar, pero se pinta mientras escribís. Con :user-invalid solo se pinta después de salir del campo.

Probá las tres opciones: escribí la mitad de un email, pasá al otro campo con Tab y apretá Continuar con los campos vacíos. Al lado de cada campo ves qué pseudoclases coinciden.

input:user-invaliden rojo: 0 de 2

Paso 2 de 3 · Envío

¿A dónde te lo mandamos?

Falta la @ o el dominio.

:invalid
no coincide
:placeholder-shown
no coincide
:user-invalid
no coincide

Son 4 números.

:invalid
no coincide
:placeholder-shown
no coincide
:user-invalid
no coincide

el error se pinta con
envio.css
.envio .error {
display: none;
}
.envio input:user-invalid {
border-color: oklch(0.58 0.2 27);
}
.envio input:user-invalid + .error {
display: block;
}
envio.html
<label for="email">Email</label>
<input id="email" type="email" required
class="peer border user-invalid:border-[oklch(0.58_0.2_27)]">
<p class="hidden peer-user-invalid:block">Falta la @ o el dominio.</p>

El mensaje de error está siempre en el HTML y el CSS solo lo muestra. Conectalo al campo con aria-describedby para que el lector de pantalla lo lea junto con la etiqueta.

Campos autocompletados

Cuando el navegador completa un campo con datos guardados, el campo coincide con :autofill y le pinta un fondo propio (amarillo o celeste) con mucha prioridad. No se puede pisar con background; el truco de siempre es taparlo con una sombra interior del tamaño del campo.

autofill.css
input:-webkit-autofill,
input:autofill {
box-shadow: inset 0 0 0 100px oklch(0.97 0.02 155);
-webkit-text-fill-color: oklch(0.25 0 0);
}

:autofill sin prefijo está desde Chrome 110 y Safari 15, y Firefox lo soporta en parte; por eso conviene escribir también :-webkit-autofill, que entienden todos. No es Baseline.

Campos que crecen

Un textarea tiene un alto fijo y un input un ancho fijo, escribas lo que escribas. Con field-sizing: content se miden por su contenido, como un elemento cualquiera: el textarea crece con cada línea y el input con cada letra. Antes esto pedía JavaScript.

Ponele límites: min-height para que vacío no quede de una línea y max-height para que, pasado el tope, aparezca el scroll. La unidad lh mide en alturas de línea, justo lo que hace falta acá.

field-sizing: contentescribí y el campo crece

Datos para la entrega

entrega.css
.entrega textarea,
.entrega input {
field-sizing: content;
}
.entrega textarea {
min-height: 2lh;
max-height: 5lh;
}
.entrega input {
min-width: 3ch;
max-width: 100%;
}
entrega.html
<input class="field-sizing-content min-w-[3ch] max-w-full" placeholder="4B">
<textarea class="field-sizing-content min-h-[2lh] max-h-[5lh]">…</textarea>

field-sizing es Baseline desde junio de 2026 (Chrome 123, Firefox 152, Safari 26.2). En un navegador anterior el campo simplemente queda fijo, así que no rompe nada.

Un select a tu medida

El select nativo siempre fue el control más difícil de estilar: la lista desplegable la dibuja el sistema. Con appearance: base-select (en el select y en su lista, ::picker(select)) pasa a ser HTML común: el botón, la lista y cada option aceptan CSS. Aparecen pseudoelementos nuevos como ::picker-icon (la flecha) y ::checkmark (el tilde de la opción elegida), y :open coincide mientras la lista está abierta.

appearance
comprobando base-select…

Entrega el jueves 16

Te avisamos por email 30 minutos antes.

horario.css
/* sin soporte, el bloque no aplica y queda el select nativo */
@supports (appearance: base-select) {
.horario select,
.horario ::picker(select) {
appearance: base-select;
}
.horario ::picker(select) {
border: 1px solid oklch(0.5 0 0 / 25%);
border-radius: 10px;
padding: 4px;
box-shadow: 0 8px 24px oklch(0 0 0 / 12%);
}
.horario option {
border-radius: 6px;
}
.horario option:checked {
font-weight: 500;
}
.horario select::picker-icon {
transition: rotate 150ms cubic-bezier(0.22, 1, 0.36, 1);
}
.horario select:open::picker-icon {
rotate: 180deg;
}
}
horario.html
<!-- sin soporte, el bloque no aplica y queda el select nativo -->
<select class="[appearance:base-select] [&::picker(select)]:[appearance:base-select]
[&::picker(select)]:border [&::picker(select)]:border-[oklch(0.5_0_0/25%)]
[&::picker(select)]:rounded-[10px] [&::picker(select)]:p-1
[&::picker(select)]:shadow-[0_8px_24px_oklch(0_0_0/12%)]
[&_option]:rounded-[6px] [&_option:checked]:font-medium
[&::picker-icon]:transition-[rotate] [&::picker-icon]:duration-150 open:[&::picker-icon]:rotate-180">
<option>9 a 12 h · mañana</option>
<option>12 a 15 h · mediodía</option>
<option>15 a 18 h · tarde</option>
<option>18 a 21 h · noche</option>
</select>

Todavía no es Baseline: está en Chrome 135 y Safari 27, y en Firefox solo detrás de una opción experimental. Envolvé todo en @supports (appearance: base-select): donde no hay soporte, queda el select nativo, que funciona igual.

En Tailwind

Tailwind tiene utilidades o variantes para casi todo lo de esta lección:

CSSTailwind
accent-color · caret-coloraccent-green-600 · caret-green-600
appearance: noneappearance-none
:checked · ::file-selector-buttonchecked: · file:
:user-invalid · :user-validuser-invalid: · user-valid:
:placeholder-shown · :autofillplaceholder-shown: · autofill:
mensaje al lado del campopeer-user-invalid:
field-sizing: contentfield-sizing-content
max-height: 5lhmax-h-[5lh]
appearance: base-select[appearance:base-select]
envio.html
<input type="email" required
class="peer border user-invalid:border-red-600
user-valid:border-green-600">
<p class="hidden peer-user-invalid:block">Falta la @ o el dominio.</p>
<textarea class="field-sizing-content min-h-[2lh] max-h-[5lh]"></textarea>
<form class="accent-green-600 caret-green-600">…</form>