Estilos para formularios con CSS
En esta lección
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;
}
:focusse activa cuando el elemento tiene el foco, por cualquier motivo.:focus-visiblese 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: nonesin poner otro indicador de foco. Quien navega con el teclado se queda sin saber en qué campo está. Si eloutlinepor 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;
}
| Pseudoclase | Cuándo se activa |
|---|---|
:valid | El valor cumple las reglas (también si aún no has tocado nada) |
:invalid | No las cumple, desde que se carga la página |
:user-valid | Cumple las reglas y el usuario ya ha interactuado |
:user-invalid | No 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
outlinedel 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
:invaliddesde 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. Usaaccent-coloroappearance: 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écnica | Para qué sirve | Ejemplo |
|---|---|---|
font: inherit | Campos con la fuente de la página | input, button { font: inherit; } |
box-sizing | Que width: 100% no desborde | border-box |
:focus-visible | Foco visible con teclado | outline: 3px solid ... |
:hover, :active | Respuesta al ratón y al pulsar | Cambio de fondo |
:disabled | Campo desactivado | opacity: 0.5 |
:checked | Casilla marcada | :checked + label |
:placeholder-shown | Campo vacío | :not(:placeholder-shown) |
:user-invalid | Error tras interactuar | Borde rojo |
accent-color | Color de casillas y rangos | accent-color: #e76f51 |
appearance: none | Diseño propio del control | Casilla personalizada |
| Grid | Una o dos columnas | grid-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).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.