Clase 33 · Efectos CSS y extras
Seis extras para rematar un proyecto: estados de formulario, @property, transformaciones 3D, recortes y formas, efecto cristal y contadores CSS. Cada bloque termina con su demo en funcionamiento.
Pseudoclases de formulario
En la clase 23 vimos cómo se construye un formulario; ahora vemos cómo reacciona.
Las pseudoclases cambian el estilo según el estado del campo, igual que :hover lo hace con el ratón: :focus, :checked, :disabled, :required, :valid/:invalid…
:focus, :focus-visible y :focus-within
El campo (o el formulario) tiene el foco.
/* el elemento tiene el foco (clic o teclado) */
input:focus {
border-color: #3a8dde;
}
/* foco solo cuando se navega con TECLADO (no al hacer clic con el ratón) */
input:focus-visible {
outline: 3px solid #92d050;
}
/* el formulario cuando cualquiera de sus campos tiene el foco */
.formulario:focus-within {
background-color: #f6fbee;
}
Nunca quites el outline sin dar otra señal de foco: quien navega con teclado deja de saber dónde está.
:checked, :disabled y accent-color
Casillas marcadas y campos bloqueados.
/* casilla marcada: la etiqueta cambia con :has() */
.check:has(input:checked) {
color: #6ea832;
}
/* color de la casilla/radio sin rehacerlos */
input[type="checkbox"] {
accent-color: #6ea832;
}
/* campos deshabilitados */
input:disabled {
background-color: #f2f4ee;
}
:required, :valid e :invalid
Estilo según la validación del navegador.
Los atributos required, type="email"… hacen que el navegador valide el campo. Con CSS podemos reflejarlo. Mejor usar :user-valid y :user-invalid, que esperan a que el usuario haya escrito antes de marcar un error.
/* campos obligatorios y opcionales */
input:required {
border-left: 4px solid #e07a3c;
}
input:optional {
border-left: 4px solid #d8e0e9;
}
/* :valid / :invalid se evalúan desde el principio (campo vacío = rojo) */
input:invalid {
border-color: #d80606;
}
/* :user-valid / :user-invalid esperan a que el usuario haya escrito */
input:user-invalid {
border-color: #d80606;
}
input:user-valid {
border-color: #4caf50;
}
::placeholder y caret-color
El texto de ayuda y el cursor de escritura.
input::placeholder {
color: #a0a8b3;
font-style: italic;
}
/* color del cursor de escritura */
input {
caret-color: #6ea832;
}
Formulario en funcionamiento
Haz clic en los campos, escribe un correo incorrecto y marca la casilla.
<form class="formulario">
<label>Nombre
<input type="text" placeholder="Tu nombre" required>
</label>
<label>Email
<input type="email" placeholder="tu@correo.com" required>
</label>
<label class="check">
<input type="checkbox"> Acepto las condiciones
</label>
<label>Campo bloqueado
<input type="text" value="No editable" disabled>
</label>
<button type="submit">Enviar</button>
</form>
.formulario {
display: grid;
gap: 14px;
max-width: 340px;
padding: 20px;
border-radius: 12px;
transition: 0.3s;
}
.formulario label {
display: grid;
gap: 4px;
font-weight: 600;
}
input:not([type="checkbox"]) {
padding: 10px;
border: 2px solid #d8e0e9;
border-radius: 8px;
font: inherit;
caret-color: #6ea832;
}
input::placeholder {
color: #a0a8b3;
font-style: italic;
}
input:focus-visible {
outline: 3px solid #92d050;
outline-offset: 2px;
}
input:user-valid {
border-color: #4caf50;
}
input:user-invalid {
border-color: #d80606;
}
input:disabled {
background-color: #f2f4ee;
color: #999;
}
input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: #6ea832;
}
.formulario:focus-within {
background-color: #f6fbee;
}
.check:has(input:checked) {
color: #6ea832;
}
.check {
display: flex;
align-items: center;
gap: 8px;
}
Variables que sí se animan
Las variables de la clase 14 no se pueden animar; con @property sí.
Una variable normal es solo texto para el navegador: no sabe que 0deg y 360deg son ángulos y, por tanto, no puede calcular los valores intermedios. Con @property le damos un tipo.
/* Una variable normal no tiene "tipo": el navegador no sabe interpolarla */
.borde {
--angulo: 0deg;
animation: girar 3s linear infinite;
}
@keyframes girar {
to {
--angulo: 360deg; /* salta de 0 a 360 de golpe */
}
}
La regla @property
Nombre, tipo, herencia y valor inicial.
syntax: el tipo de valor.inherits: si los hijos heredan la variable.initial-value: valor por defecto (obligatorio salvo que la sintaxis sea*).
@property --angulo {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
/* otros tipos habituales */
syntax: "<color>";
syntax: "<length>";
syntax: "<percentage>";
syntax: "<number>";
@property en funcionamiento
Un borde con degradado cónico que gira, animando solo el ángulo.
<div class="borde">Borde animado</div>
@property --angulo {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.borde {
width: 240px;
height: 140px;
display: grid;
place-items: center;
font-weight: 700;
border: 4px solid transparent;
border-radius: 16px;
background: linear-gradient(#fff, #fff) padding-box,
conic-gradient(from var(--angulo), #92d050, #3a8dde, #8e5bd6, #92d050) border-box;
animation: girar 3s linear infinite;
}
@keyframes girar {
to {
--angulo: 360deg;
}
}
Profundidad con CSS
En la clase 10 vimos translate, rotate y scale en 2D. Añadimos el eje Z.
Para que un giro en 3D se vea con profundidad el contenedor necesita perspective: es la distancia a la que está el «observador». Cuanto menor el valor, más acentuado el efecto.
.contenedor {
perspective: 800px;
}
.caja {
transform: rotateY(40deg);
}
rotateX, rotateY y translateZ
Los tres ejes.
/* ejes: X (gira "hacia delante"), Y (gira "de lado"), Z (como rotate) */
transform: rotateX(45deg);
transform: rotateY(45deg);
transform: rotateZ(45deg);
/* acercar o alejar en profundidad */
transform: translateZ(60px);
preserve-3d y backface-visibility
Lo necesario para una tarjeta que se da la vuelta.
transform-style: preserve-3d: los hijos mantienen su posición 3D dentro del padre.backface-visibility: hidden: oculta la cara cuando está «de espaldas».
La tarjeta tiene dos caras superpuestas con position: absolute: el reverso ya empieza girado 180°.
.flip-inner {
transform-style: preserve-3d;
}
.cara {
backface-visibility: hidden;
}
Tarjeta 3D en funcionamiento
Pasa el ratón (o pulsa) sobre la tarjeta.
<div class="flip" tabindex="0">
<div class="flip-inner">
<div class="cara frente">Frente</div>
<div class="cara reverso">Reverso</div>
</div>
</div>
.flip {
width: 220px;
height: 140px;
perspective: 800px;
}
.flip-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.8s;
transform-style: preserve-3d;
}
.flip:hover .flip-inner,
.flip:focus .flip-inner {
transform: rotateY(180deg);
}
.cara {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: #fff;
border-radius: 12px;
backface-visibility: hidden;
}
.frente {
background-color: #3a8dde;
}
.reverso {
background-color: #e07a3c;
transform: rotateY(180deg);
}
clip-path
Recortar un elemento con la forma que queramos.
Lo que queda fuera de la forma desaparece (y deja de responder al ratón). Las formas más usadas son circle(), ellipse(), inset() y polygon().
/* círculo y elipse */
clip-path: circle(50%);
clip-path: ellipse(50% 35% at 50% 50%);
/* rectángulo con las esquinas recortadas hacia dentro */
clip-path: inset(10px 20px round 12px);
/* polígono: lista de puntos "x y" (arriba-izquierda es 0 0) */
clip-path: polygon(50% 0, 100% 100%, 0 100%);
Para dibujar un polygon() sin calcular puntos a mano, usa un generador como Clippy o las herramientas del navegador.
Sección con corte diagonal
El uso más típico de polygon() en una web real.
Se recorta el borde inferior del bloque con un polígono: los dos puntos de abajo están a alturas distintas (80% y 100%).
shape-outside
Que el texto rodee una forma y no una caja.
Funciona solo en elementos con float. Con float: left el elemento se coloca a la izquierda y el texto lo rodea; shape-outside hace que lo rodee siguiendo su forma.
.bola {
float: left;
width: 120px;
height: 120px;
border-radius: 50%;
shape-outside: circle(50%);
}
Recortes en funcionamiento
Cuatro formas, una sección diagonal y un texto que rodea un círculo.
<div class="formas">
<div class="forma triangulo"></div>
<div class="forma circulo"></div>
<div class="forma hexagono"></div>
<div class="forma estrella"></div>
</div>
<section class="diagonal">
<h2>Sección con corte diagonal</h2>
</section>
<div class="texto-forma">
<div class="bola"></div>
<p>El texto rodea la forma circular: no la caja cuadrada que la contiene. Esto se consigue con la propiedad shape-outside, que solo funciona sobre elementos con float. Cambiando la forma, el texto cambia su recorrido.</p>
</div>
.formas {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.forma {
width: 110px;
height: 110px;
background-image: linear-gradient(135deg, #3a8dde, #8e5bd6);
}
.triangulo {
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
.circulo {
clip-path: circle(50%);
}
.hexagono {
clip-path: polygon(25% 5%, 75% 5%, 100% 50%, 75% 95%, 25% 95%, 0 50%);
}
.estrella {
clip-path: polygon(50% 0, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
.diagonal {
padding: 40px 24px 70px;
color: #fff;
background-color: #364f6b;
clip-path: polygon(0 0, 100% 0, 100% 80%, 0 100%);
}
.diagonal h2 {
margin: 0;
}
.bola {
float: left;
width: 120px;
height: 120px;
margin-right: 16px;
border-radius: 50%;
background-color: #e07a3c;
shape-outside: circle(50%);
}
backdrop-filter
Un filtro sobre lo que hay detrás del elemento.
En la clase 10 vimos filter, que afecta al propio elemento. backdrop-filter afecta a lo que se ve a través de él: es el efecto «cristal esmerilado» (glassmorphism).
/* filter: desenfoca el propio elemento y su contenido */
.caja {
filter: blur(8px);
}
/* backdrop-filter: desenfoca lo que hay DETRÁS del elemento */
.cristal {
backdrop-filter: blur(12px);
}
La receta del cristal
Fondo semitransparente + desenfoque.
El elemento necesita un fondo semitransparente (rgba); si fuera opaco no se vería nada detrás. Se completa con un borde claro para dar sensación de cristal.
.cristal {
background-color: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(12px) saturate(1.5);
}
Safari antiguo necesita el prefijo -webkit-backdrop-filter. Y recuerda: sin contenido de colores detrás no hay nada que desenfocar.
Efecto cristal en funcionamiento
Una tarjeta de cristal sobre formas de colores.
<div class="fondo">
<div class="cristal">
<h3>Efecto cristal</h3>
<p>El fondo que hay detrás se ve desenfocado.</p>
</div>
</div>
.fondo {
position: relative;
display: grid;
place-items: center;
height: 260px;
overflow: hidden;
background-image: linear-gradient(135deg, #3a8dde, #8e5bd6, #e07a3c);
border-radius: 12px;
}
.fondo::before {
content: "";
position: absolute;
top: 24px;
left: 70px;
width: 140px;
height: 140px;
border-radius: 50%;
background-color: #ffd54f;
}
.fondo::after {
content: "";
position: absolute;
bottom: 12px;
right: 90px;
width: 120px;
height: 120px;
border-radius: 50%;
background-color: #4caf7a;
}
.cristal {
position: relative;
z-index: 1;
padding: 24px 32px;
color: #fff;
background-color: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 16px;
backdrop-filter: blur(12px);
}
.cristal h3 {
margin: 0 0 6px;
}
.cristal p {
margin: 0;
}
Numerar con CSS
En la clase 19 personalizamos las listas; los contadores numeran cualquier elemento.
Con list-style solo podemos numerar listas. Los contadores numeran títulos, tarjetas, pasos… y permiten numeración anidada tipo «1.1», «1.2».
counter-reset, counter-increment y counter()
Los tres pasos.
counter-reset: crea el contador (en el elemento padre).counter-increment: suma 1 cada vez que aparece el elemento.counter(): se muestra dentro decontent, normalmente en un::before.
/* 1. Se crea el contador en el elemento padre */
.pasos {
counter-reset: paso;
}
/* 2. Se suma 1 en cada elemento */
.pasos li {
counter-increment: paso;
}
/* 3. Se muestra con content */
.pasos li::before {
content: counter(paso);
}
/* números romanos, letras... (como list-style-type) */
.pasos li::before {
content: counter(paso, upper-roman) ". ";
}
.pasos li::before {
content: counter(paso, lower-alpha) ") ";
}
Contadores en funcionamiento
Capítulos con apartados («1.1») y una lista de pasos con círculos.
<article class="curso">
<h2>Introducción</h2>
<h3>Qué es HTML</h3>
<h3>Qué es CSS</h3>
<h2>Selectores</h2>
<h3>Clases</h3>
<h3>ID</h3>
</article>
<ol class="pasos">
<li>Crear el archivo HTML</li>
<li>Enlazar el archivo CSS</li>
<li>Dar estilo a los elementos</li>
</ol>
.curso {
counter-reset: capitulo;
}
.curso h2 {
counter-increment: capitulo;
counter-reset: apartado;
}
.curso h2::before {
content: counter(capitulo) ". ";
color: #6ea832;
}
.curso h3 {
counter-increment: apartado;
}
.curso h3::before {
content: counter(capitulo) "." counter(apartado) " ";
color: #999;
}
.pasos {
padding: 0;
list-style: none;
counter-reset: paso;
}
.pasos li {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
counter-increment: paso;
}
.pasos li::before {
content: counter(paso);
flex: none;
width: 32px;
height: 32px;
display: grid;
place-items: center;
font-weight: 700;
color: #1c2b0d;
background-color: #92d050;
border-radius: 50%;
}