Saltar al contenido
formularios-css.css · devschool

Estilos para formularios con CSS

Lección 17 de 25 · 10 min de lectura · Actualizado el

En esta lección
  1. La base: que los campos se comporten
  2. Estados con pseudoclases
  3. Validación: :valid, :invalid y :user-invalid
  4. Casillas, opciones y desplegables
  5. Botones
  6. Maquetar el formulario con grid
  7. Errores frecuentes
  8. Resumen

Los formularios son la parte de una web donde el usuario hace algo: registrarse, comprar, buscar, reservar una clase. Si los campos son pequeños, el foco no se ve o los errores no se entienden, la gente abandona. En esta lección aprenderás a dar a los formularios un aspecto limpio y coherente, a mostrar cada estado (foco, error, desactivado, marcado) y a colocarlos en una o dos columnas.

Los elementos y atributos del formulario (<label>, tipos de <input>, required…) están en formularios y validación de formularios del curso de HTML. Aquí nos centramos en el CSS.

La base: que los campos se comporten

font: inherit

Los campos de formulario no heredan la fuente de la página. Cada navegador les pone la suya, normalmente más pequeña (unos 13 px). Por eso el texto que escribes en un <input> se ve distinto al resto:

input,
select,
textarea,
button {
  font: inherit;   /* misma fuente, tamaño y grosor que su padre */
}

Es una de las primeras reglas que conviene escribir en cualquier proyecto.

box-sizing: border-box

Si le das a un campo width: 100% y además padding y border, con el modelo de caja por defecto acaba midiendo más del 100 % y se sale del formulario:

*,
*::before,
*::after {
  box-sizing: border-box;   /* el ancho incluye padding y borde */
}

Lo tienes explicado en el modelo de caja.

Un estilo común para los campos de texto

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
select,
textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid #b8c2d1;
  border-radius: 8px;
  background: white;
  color: #1b2d34;
}

textarea {
  min-height: 120px;
  resize: vertical;   /* que solo se pueda estirar hacia abajo */
}

label {
  display: block;
  margin-bottom: 4px;
  font-weight: 600;
}

Usamos selectores de atributo (input[type="text"]) para no aplicar este estilo a casillas y botones de opción, que tienen otra forma.

16 px en el móvil

Si un campo tiene una letra menor de 16 px, Safari en iPhone hace zoom al tocarlo, y el usuario tiene que alejar la página a mano después. Con font: inherit y un texto base de 1rem ya lo evitas. No bajes el tamaño de los campos por estética.

Estados con pseudoclases

Un campo pasa por muchos estados, y el usuario debe ver en cuál está. CSS tiene una pseudoclase para cada uno.

:hover, :focus y :focus-visible

input:hover {
  border-color: #8a97aa;
}

input:focus-visible {
  outline: 3px solid rgb(45 108 223 / 0.5);
  outline-offset: 2px;
  border-color: #2d6cdf;
}
  • :focus se activa cuando el elemento tiene el foco, por cualquier motivo.
  • :focus-visible se activa cuando el navegador cree que conviene mostrar el foco. En campos de texto es casi lo mismo que :focus; en botones, se muestra al usar el teclado pero no al hacer clic con el ratón.

Cuidado: nunca escribas outline: none sin poner otro indicador de foco. Quien navega con el teclado se queda sin saber en qué campo está. Si el outline por defecto no te gusta, cámbialo, pero no lo quites.

:disabled

input:disabled,
button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

Se aplica a los campos con el atributo disabled. Sus valores no se envían con el formulario. Existe también :read-only, para campos que se ven pero no se pueden editar.

:checked

Casillas y botones de opción marcados. Muy útil combinado con el combinador + para cambiar la etiqueta que va detrás:

input[type="checkbox"]:checked + label {
  text-decoration: line-through;   /* tarea hecha */
  color: #888;
}

:placeholder-shown y ::placeholder

input::placeholder {
  color: #8a97aa;   /* el texto de ejemplo */
}

input:not(:placeholder-shown) {
  background: #f8fbff;  /* el usuario ya ha escrito algo */
}

::placeholder (dos puntos dobles) es el texto de ejemplo. :placeholder-shown (uno) es el campo mientras ese texto se ve, es decir, mientras está vacío. Recuerda que el placeholder no sustituye a la etiqueta: desaparece al escribir.

Validación: :valid, :invalid y :user-invalid

El navegador sabe si un campo cumple sus reglas (required, type="email", min, pattern…). Con CSS puedes mostrarlo:

input:invalid { border-color: #d62839; }
input:valid   { border-color: #2a9d5c; }

Pero hay un problema: un campo required vacío es inválido desde el principio. El usuario abre la página y ve todo en rojo antes de haber escrito nada. Da la sensación de haber hecho algo mal.

La solución moderna es :user-invalid: solo se activa después de que el usuario haya interactuado con el campo (escribir y salir de él, o intentar enviar el formulario):

input:user-invalid {
  border-color: #d62839;
  background: #fff5f6;
}

input:user-valid {
  border-color: #2a9d5c;
}
PseudoclaseCuándo se activa
:validEl valor cumple las reglas (también si aún no has tocado nada)
:invalidNo las cumple, desde que se carga la página
:user-validCumple las reglas y el usuario ya ha interactuado
:user-invalidNo las cumple y el usuario ya ha interactuado

Consejo: no confíes solo en el color para marcar un error. Muchas personas no distinguen bien el rojo del verde. Añade un icono, un texto o un borde más grueso.

Casillas, opciones y desplegables

accent-color: la forma rápida

Casillas, botones de opción, barras de rango y de progreso tienen un color propio del sistema (normalmente azul). Con una sola línea lo cambias a tu color de marca:

input[type="checkbox"],
input[type="radio"],
input[type="range"],
progress {
  accent-color: #e76f51;
}

Conservan todo su comportamiento y accesibilidad. Si solo quieres cambiar el color, no necesitas más.

appearance: none: empezar de cero

Si quieres un diseño totalmente propio, appearance: none quita el aspecto nativo del control y lo deja como una caja vacía que puedes decorar:

.casilla {
  appearance: none;
  width: 1.25em;
  height: 1.25em;
  margin: 0;
  border: 2px solid #8a97aa;
  border-radius: 4px;
  display: inline-grid;
  place-content: center;
  vertical-align: -0.2em;
  cursor: pointer;
}

.casilla::before {
  content: "";
  width: 0.65em;
  height: 0.65em;
  background: white;
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); /* una marca de verificación */
  transform: scale(0);
  transition: transform 0.15s;
}

.casilla:checked {
  background: #2d6cdf;
  border-color: #2d6cdf;
}

.casilla:checked::before {
  transform: scale(1);
}

.casilla:focus-visible {
  outline: 3px solid rgb(45 108 223 / 0.5);
  outline-offset: 2px;
}
<label>
  <input class="casilla" type="checkbox" name="newsletter">
  Quiero recibir el boletín
</label>

¿Por qué es accesible? Porque sigue siendo un <input type="checkbox"> de verdad: se marca con el teclado (barra espaciadora), lo anuncian los lectores de pantalla, se envía con el formulario y la etiqueta sigue funcionando al hacer clic. Solo hemos cambiado cómo se dibuja. Nunca sustituyas una casilla por un <div> con JavaScript.

Los campos con appearance: none admiten ::before porque dejan de dibujarse como un control nativo. Con un <input> normal, los pseudoelementos no funcionan.

El <select>

select {
  appearance: none;
  padding-right: 36px;   /* sitio para la flecha */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23555'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 10px;
}

Al quitar la apariencia nativa desaparece la flecha, así que la dibujamos con un pequeño SVG de fondo. La lista desplegable que se abre sigue siendo la del sistema: en los navegadores actuales su personalización completa todavía está llegando, así que no dependas de ella.

Botones

.boton {
  display: inline-block;
  padding: 10px 20px;
  border: 0;
  border-radius: 8px;
  background: #2d6cdf;
  color: white;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

.boton:hover   { background: #1f55b8; }
.boton:active  { transform: translateY(1px); }   /* efecto de pulsación */
.boton:focus-visible {
  outline: 3px solid rgb(45 108 223 / 0.5);
  outline-offset: 2px;
}
.boton:disabled {
  background: #9aa7ba;
  cursor: not-allowed;
}

.boton-secundario {
  background: transparent;
  color: #2d6cdf;
  border: 2px solid currentColor;
}

Un botón debe parecer pulsable: fondo de color, cursor: pointer, cambio al pasar el ratón y al pulsar. En móvil, procura que mida al menos unos 44 px de alto para que sea fácil tocarlo con el dedo.

Maquetar el formulario con grid

Una columna

La opción más clara y la que mejor funciona en móvil: una etiqueta encima de cada campo.

.formulario {
  display: grid;
  gap: 16px;
  max-width: 480px;
}

Dos columnas en pantallas grandes

Para formularios largos (datos de envío, por ejemplo), dos columnas ahorran espacio en escritorio. Los campos cortos (código postal, ciudad) comparten fila y los largos ocupan las dos:

<form class="formulario">
  <div class="campo"><label for="nom">Nombre</label><input id="nom" type="text"></div>
  <div class="campo"><label for="ape">Apellidos</label><input id="ape" type="text"></div>
  <div class="campo ancho"><label for="dir">Dirección</label><input id="dir" type="text"></div>
  <div class="campo"><label for="cp">Código postal</label><input id="cp" type="text" inputmode="numeric"></div>
  <div class="campo"><label for="ciu">Ciudad</label><input id="ciu" type="text"></div>
  <button class="boton ancho">Continuar</button>
</form>
.formulario {
  display: grid;
  gap: 16px;
}

@media (min-width: 640px) {
  .formulario {
    grid-template-columns: 1fr 1fr;
  }
  .ancho {
    grid-column: 1 / -1;   /* de la primera a la última línea: ocupa toda la fila */
  }
}

En móvil todo va en una columna; a partir de 640 px, en dos. grid-column: 1 / -1 es la forma de decir “toda la fila” sin contar columnas. Más detalles en Grid y en diseño responsive.

Consejo: mantén el orden de lectura natural (de izquierda a derecha y de arriba abajo). El orden en que el teclado recorre los campos es el del HTML, no el visual.

Errores frecuentes

  • Olvidar font: inherit. Los campos salen con una letra más pequeña y distinta.
  • Campos que se salen del formulario. Falta box-sizing: border-box.
  • Quitar el outline del foco. Los usuarios de teclado se pierden.
  • Letra de menos de 16 px en los campos. El iPhone hace zoom al tocarlos.
  • Marcar en rojo con :invalid desde el principio. Usa :user-invalid.
  • Usar el placeholder como etiqueta. Desaparece al escribir y suele tener poco contraste.
  • Sustituir casillas por <div>. Se pierde el teclado, el lector de pantalla y el envío. Usa accent-color o appearance: none.
  • Quitar la apariencia del <select> sin dibujar la flecha. Ya no parece un desplegable.
  • Botones diminutos en móvil. Deja al menos 44 px de alto.

Resumen

TécnicaPara qué sirveEjemplo
font: inheritCampos con la fuente de la páginainput, button { font: inherit; }
box-sizingQue width: 100% no desbordeborder-box
:focus-visibleFoco visible con tecladooutline: 3px solid ...
:hover, :activeRespuesta al ratón y al pulsarCambio de fondo
:disabledCampo desactivadoopacity: 0.5
:checkedCasilla marcada:checked + label
:placeholder-shownCampo vacío:not(:placeholder-shown)
:user-invalidError tras interactuarBorde rojo
accent-colorColor de casillas y rangosaccent-color: #e76f51
appearance: noneDiseño propio del controlCasilla personalizada
GridUna o dos columnasgrid-column: 1 / -1

Ahora que has visto tantos colores repetidos (#2d6cdf aparece en casi cada regla), es el momento perfecto para aprender a guardarlos en un solo sitio con las variables CSS.

Pruébalo tú

Cambia el código y pulsa Ejecutar (o Ctrl + Enter).

formularios-css.html
Resultado

Pon a prueba lo que has aprendido

[CSS] El texto que escribes en los <input> se ve más pequeño y con otra fuente que el resto de la página. ¿Qué regla lo soluciona?

[CSS] Con esta regla, los campos obligatorios aparecen en rojo nada más cargar la página. ¿Qué pseudoclase evita ese efecto?
input:invalid {
  border-color: #d62839;
}

[CSS] Solo quieres que las casillas de tu formulario sean naranjas en lugar de azules. ¿Qué es lo más sencillo y accesible?

[CSS] En un formulario de dos columnas con grid, ¿qué hace esta regla en el campo de dirección?
.formulario { display: grid; grid-template-columns: 1fr 1fr; }
.ancho { grid-column: 1 / -1; }

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.