wandres.dev
FONDOS Y BORDES · Capas, repetición y radios

background-clip, background-origin y el coste de attachment

Las dos cajas que gobiernan un fondo y por qué son distintas, el recorte al texto y sus trampas, y por qué background-attachment fixed es una de las declaraciones más caras de CSS.

⏱ 18 min

Un fondo tiene dos cajas y casi nadie las distingue: una decide dónde se ancla la imagen y otra dónde se ve. Son propiedades distintas con valores iguales, y usarlas juntas resuelve cosas que parecen exigir un pseudoelemento. La tercera propiedad de la familia, background-attachment, tiene un valor que conviene tratar como una decisión de rendimiento y no de estética.

🎯 Al terminar esta lección sabrás
  • Separar el área de posicionamiento del área de recorte.
  • Aplicar background-clip: text con su prefijo de compatibilidad.
  • Explicar qué hace background-attachment: local y cuándo es la respuesta.
  • Justificar por qué fixed fuerza el scroll al hilo principal.

Las dos cajas

background-origin define el área de posicionamiento: el rectángulo respecto al cual se resuelven los porcentajes de posición y de tamaño, y desde cuya esquina superior izquierda empieza la repetición.

background-clip define el área de recorte: hasta dónde se pinta lo que se haya generado.

Los dos aceptan border-box, padding-box y content-box, y sus valores iniciales son distintos: background-origin vale padding-box y background-clip vale border-box.

Ese desajuste es intencionado y resuelve el caso más común. Con un borde punteado o semitransparente, quieres que el fondo se vea por debajo del borde, para que los huecos del punteado no dejen ver la página; por eso el recorte llega hasta la caja de borde. Y a la vez quieres que la imagen se ancle donde empieza el relleno, para que añadir o quitar borde no descoloque el fondo; por eso el posicionamiento es la caja de relleno.

/* El fondo llega bajo el borde punteado, y la imagen se ancla al relleno. */
.caja {
  border: 4px dashed #cba6f7;
  padding: 16px;
  background-image: url(sello.svg);
  background-repeat: no-repeat;
  /* origin: padding-box y clip: border-box, por defecto */
}

Cambiar solo el recorte produce el efecto contrario, que también es útil:

/* El borde punteado deja ver la pagina por sus huecos. */
.calada { background-clip: padding-box; }

Y como son propiedades de capa, cada capa puede tener su combinación:

.doble {
  background-image:
    linear-gradient(in oklab, #cba6f7, #89b4fa),
    linear-gradient(in oklab, #1e1e2e, #11111b);
  background-clip: content-box, border-box;
  padding: 12px;
}

La primera capa solo se ve dentro del contenido; la segunda llena la caja entera. Eso es un marco de dos colores sin pseudoelementos.

Recortar al texto

background-clip: text recorta el fondo a la forma de los glifos del elemento. Combinado con color: transparent, produce texto pintado con un degradado o con una imagen.

.titular {
  background-image: linear-gradient(in oklch to right, #89b4fa, #f38ba8);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

El prefijo sigue siendo necesario para versiones de Safari que aún no aceptan la forma sin prefijo, y no molesta en ningún otro sitio. Declara las dos, en ese orden.

Tres trampas que hay que conocer:

El fondo se dimensiona contra el elemento, no contra el texto. Un degradado horizontal en un titular de dos líneas recorre el ancho del bloque, no el de cada línea, con lo que la segunda línea puede empezar en un color que no es el del principio. Ponerle width: fit-content al elemento suele ser lo que buscabas.

color: transparent es lo que hace visible el fondo, y también lo que rompe el fallback. Si el degradado no carga o el navegador no soporta el recorte, el texto queda invisible. La defensa es declarar el color transparente solo dentro de @supports:

.titular { color: #cdd6f4; }

@supports (background-clip: text) or (-webkit-background-clip: text) {
  .titular {
    background-image: linear-gradient(in oklch to right, #89b4fa, #f38ba8);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

La selección y el subrayado se comportan raro. El texto sigue ahí para el lector de pantalla y para el buscador, que es lo importante, pero la selección del ratón pinta sobre un texto transparente y el resultado depende del navegador. Para bloques largos no es la herramienta.

Attachment y su coste

background-attachment tiene tres valores y dos de ellos son baratos.

scroll, el inicial, fija el fondo respecto a la caja de borde del elemento. Si el elemento tiene desbordamiento con scroll propio, el fondo no se mueve con el contenido.

local fija el fondo respecto al contenido, así que sí se mueve al hacer scroll dentro del elemento. Es el que quieres para un patrón que tenga que acompañar al texto, y también la mitad del truco clásico de las sombras que aparecen al desbordar:

/* Sombras que aparecen en los bordes cuando hay mas contenido. */
.desplazable {
  overflow: auto;
  background:
    linear-gradient(#1e1e2e 30%, transparent) top / 100% 24px no-repeat local,
    linear-gradient(transparent, #1e1e2e 70%) bottom / 100% 24px no-repeat local,
    radial-gradient(farthest-side at 50% 0, #00000060, transparent) top / 100% 12px no-repeat scroll,
    radial-gradient(farthest-side at 50% 100%, #00000060, transparent) bottom / 100% 12px no-repeat scroll;
  background-color: #1e1e2e;
}

Las capas local se desplazan con el contenido y tapan las capas scroll, que están quietas. Cuando llegas arriba del todo, la capa opaca cubre la sombra; en cuanto bajas, la sombra asoma. Cuatro degradados y ningún JavaScript.

fixed fija el fondo respecto al viewport. La imagen no se mueve al hacer scroll y el elemento se comporta como una ventana sobre ella. Y aquí llega el coste.

Con fixed, el área de posicionamiento pasa a ser el viewport, no el elemento. Eso significa que la relación entre el elemento y el fondo cambia en cada fotograma de scroll, y por tanto el fondo del elemento hay que volver a pintarlo en cada fotograma. Un scroll normal lo resuelve el compositor desplazando texturas ya rasterizadas; un elemento con background-attachment: fixed no puede desplazarse así, porque su contenido pintado depende de la posición de scroll.

El resultado es que el navegador tiene que repintar durante el scroll, y en algunas rutas de código eso obliga a llevar el scroll al hilo principal en vez de resolverlo en el compositor. Si además hay JavaScript ocupando ese hilo, el scroll se entrecorta. En dispositivos móviles el efecto es lo bastante malo como para que Safari en iOS lleve años tratando este valor de forma especial y no siempre respetarlo.

⚠️
El efecto que buscabas casi siempre se hace con position sticky

El parallax de fondo fijo con background-attachment: fixed tiene dos alternativas más baratas. Una es un elemento con position: sticky y z-index negativo, que el compositor sí sabe mover. La otra, si de verdad quieres desplazamientos a distinta velocidad, es un contenedor con perspective y capas con translateZ, que se resuelve entero en la GPU. Las dos cuestan más código y ninguna de las dos repinta.

Clip y origin no son dos formas de decir lo mismo, y la prueba es que puedes invertirlos

El error de modelo que hace que estas dos propiedades se confundan es pensar que las dos “recortan”. Solo una lo hace. background-origin no recorta nada: coloca un sistema de coordenadas, y una imagen anclada a la caja de contenido puede perfectamente pintarse fuera de ella si el recorte lo permite. background-clip no coloca nada: decide dónde se deja de pintar lo que ya está colocado. Son un origen y una máscara, y son ortogonales de verdad, no en teoría. La demostración es que la combinación cruzada —background-origin: content-box con background-clip: border-box— tiene sentido y hace algo útil: un mosaico que empieza a contar desde el contenido y se sale hasta el borde, con lo que la fila y la columna que quedan bajo el relleno son continuaciones del patrón en lugar de mosaicos partidos. Ese detalle es lo que separa una cuadrícula de fondo que se ve bien de una que tiene un margen de medio mosaico en dos lados. Y hay un tercer nivel del que casi nadie se acuerda: el recorte respeta border-radius. Cuando un elemento tiene esquinas redondeadas, background-clip: border-box recorta con la curva exterior, padding-box con la curva interior —que es la exterior menos el grosor del borde— y content-box con esa misma curva reducida otra vez por el relleno. Ahí está la explicación de por qué un fondo con background-clip: padding-box en un elemento redondeado deja ver una fina rendija del color de la página entre el borde y el fondo: no es un fallo de antialiasing, es que las dos curvas son concéntricas pero no paralelas, y en las esquinas la separación entre ellas es mayor que el grosor del borde. La solución no es subir el radio: es usar border-box y dejar que el borde se pinte encima.

⚔️ Separa las dos cajas
  1. Pon un borde punteado grueso y compara background-clip: border-box con padding-box.
  2. Cruza background-origin: content-box con background-clip: border-box en un mosaico y observa la continuidad.
  3. Escribe un titular con background-clip: text y su bloque @supports de protección.
  4. Monta las sombras de desbordamiento con las cuatro capas y local.
  5. Pon background-attachment: fixed en un elemento grande y mira el panel de rendimiento mientras haces scroll.