Saltar al contenido principal
Leyendo: Efectos CSS y extras
Apuntes de clase

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.

01 · Estados de formulario

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…

01 · Estados de formulario

:focus, :focus-visible y :focus-within

El campo (o el formulario) tiene el foco.

style.css
/* 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á.

01 · Estados de formulario

:checked, :disabled y accent-color

Casillas marcadas y campos bloqueados.

style.css
/* 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;
}
01 · Estados de formulario

: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.

style.css
/* 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;
}
01 · Estados de formulario

::placeholder y caret-color

El texto de ayuda y el cursor de escritura.

style.css
input::placeholder {
  color: #a0a8b3;
  font-style: italic;
}

/* color del cursor de escritura */
input {
  caret-color: #6ea832;
}
01 · Estados de formulario

Formulario en funcionamiento

Haz clic en los campos, escribe un correo incorrecto y marca la casilla.

index.html
<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>
style.css
.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;
}
Resultado: estados de formulario
02 · @property: variables con tipo

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.

style.css · el problema
/* 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 */
  }
}
02 · @property: variables con tipo

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 *).
style.css
@property --angulo {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
tipos de sintaxis
/* otros tipos habituales */
syntax: "<color>";
syntax: "<length>";
syntax: "<percentage>";
syntax: "<number>";
02 · @property: variables con tipo

@property en funcionamiento

Un borde con degradado cónico que gira, animando solo el ángulo.

index.html
<div class="borde">Borde animado</div>
style.css
@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;
  }
}
Resultado: @property
03 · Transformaciones 3D

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.

style.css
.contenedor {
  perspective: 800px;
}
.caja {
  transform: rotateY(40deg);
}
03 · Transformaciones 3D

rotateX, rotateY y translateZ

Los tres ejes.

style.css
/* 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);
03 · Transformaciones 3D

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°.

style.css
.flip-inner {
  transform-style: preserve-3d;
}
.cara {
  backface-visibility: hidden;
}
03 · Transformaciones 3D

Tarjeta 3D en funcionamiento

Pasa el ratón (o pulsa) sobre la tarjeta.

index.html
<div class="flip" tabindex="0">
  <div class="flip-inner">
    <div class="cara frente">Frente</div>
    <div class="cara reverso">Reverso</div>
  </div>
</div>
style.css
.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);
}
Resultado: tarjeta 3D
04 · Recortes y formas

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().

style.css
/* 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.

04 · Recortes y formas

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%).

04 · Recortes y formas

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.

style.css
.bola {
  float: left;
  width: 120px;
  height: 120px;
  border-radius: 50%;
  shape-outside: circle(50%);
}
04 · Recortes y formas

Recortes en funcionamiento

Cuatro formas, una sección diagonal y un texto que rodea un círculo.

index.html
<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>
style.css
.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%);
}
Resultado: recortes y formas
05 · Efecto cristal

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).

style.css
/* 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);
}
05 · Efecto cristal

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.

style.css
.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.

05 · Efecto cristal

Efecto cristal en funcionamiento

Una tarjeta de cristal sobre formas de colores.

index.html
<div class="fondo">
  <div class="cristal">
    <h3>Efecto cristal</h3>
    <p>El fondo que hay detrás se ve desenfocado.</p>
  </div>
</div>
style.css
.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;
}
Resultado: backdrop-filter
06 · Contadores CSS

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».

06 · Contadores CSS

counter-reset, counter-increment y counter()

Los tres pasos.

  1. counter-reset: crea el contador (en el elemento padre).
  2. counter-increment: suma 1 cada vez que aparece el elemento.
  3. counter(): se muestra dentro de content, normalmente en un ::before.
style.css
/* 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);
}
style.css · estilos de número
/* números romanos, letras... (como list-style-type) */
.pasos li::before {
  content: counter(paso, upper-roman) ". ";
}
.pasos li::before {
  content: counter(paso, lower-alpha) ") ";
}
06 · Contadores CSS

Contadores en funcionamiento

Capítulos con apartados («1.1») y una lista de pasos con círculos.

index.html
<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>
style.css
.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%;
}
Resultado: contadores CSS