La función linear(): curvas arbitrarias en CSS
Qué es linear(), en qué se diferencia de la palabra clave del mismo nombre, cómo se leen sus paradas y qué permite expresar que cubic-bezier no puede.
Una curva de Bézier cúbica solo puede tener una inflexión. Eso la incapacita para representar cualquier movimiento que oscile, rebote o cambie de dirección más de una vez, que es exactamente la familia de movimientos que más se parecen a la física real. Durante veinte años, la única salida fue JavaScript. Desde 2023, los cuatro motores implementan linear(), una función de suavizado que aproxima cualquier curva con una poligonal, y con ella se pueden expresar en CSS puro movimientos que antes exigían una librería.
- Distinguir la función
linear()de la palabra clavelinear. - Leer y escribir una lista de paradas con posiciones explícitas.
- Construir mesetas y saltos combinando posiciones repetidas.
- Estimar cuántos puntos hacen falta para una precisión dada.
No confundir con la palabra clave
linear a secas es una palabra clave: velocidad constante, equivalente a cubic-bezier(0, 0, 1, 1). linear() con paréntesis es una función que recibe una lista de valores de progreso y construye una curva poligonal que pasa por todos ellos.
.a { transition-timing-function: linear; } /* palabra clave */
.b { transition-timing-function: linear(0, 0.6, 1); } /* funcion */
La segunda produce un movimiento que llega al 60 por ciento del recorrido en la mitad del tiempo y recorre el 40 restante en la otra mitad: dos tramos rectos con velocidades distintas. Es decir, linear() no es lineal en el sentido de velocidad constante; es lineal a trozos.
Que la curva sea poligonal y no suave importa poco en la práctica: con suficientes puntos, la diferencia entre una poligonal y la curva que aproxima está por debajo del umbral de percepción, y por debajo de la resolución de un fotograma. Con veinte puntos, cada tramo dura un veinteavo de la duración, es decir un fotograma o dos a 60 hercios.
La sintaxis completa
Cada entrada de la lista es un número, opcionalmente seguido de una o dos posiciones en porcentaje.
Sin posiciones, los valores se reparten uniformemente entre 0 y 100 por ciento:
/* Cuatro paradas a 0%, 33.3%, 66.7% y 100%. */
linear(0, 0.4, 0.9, 1)
Con una posición, fijas dónde va esa parada. Las que no la llevan se interpolan uniformemente entre las que sí:
/* La parada de 0.9 ocurre al 40% del tiempo, no al 66.7%. */
linear(0, 0.4, 0.9 40%, 1)
Con dos posiciones, creas una meseta: el valor se mantiene entre las dos:
/* El progreso se queda quieto en 0.5 entre el 30% y el 60% del tiempo. */
linear(0, 0.5 30% 60%, 1)
Las mesetas son la forma de expresar una pausa a mitad de una animación sin partirla en dos, y son imposibles con Bézier.
Y el detalle que abre el capítulo siguiente: los valores no están acotados. Puedes escribir números mayores que 1 o menores que 0, y ahí es donde aparece el rebote:
/* Un rebote de tres impactos, decreciendo. */
.pelota {
animation: caer 1.4s linear(
0, 0.35 12%, 0.9 24%, 1 30%,
0.72 40%, 1 50%,
0.88 58%, 1 66%,
0.96 72%, 1 80%, 1
) forwards;
}
@keyframes caer { to { translate: 0 240px; } }
Esa curva es literalmente imposible de expresar con cubic-bezier: tiene cuatro cambios de dirección. Con linear() es una lista de números.
Cuántos puntos hacen falta
La precisión de la aproximación depende de la densidad de puntos, y el criterio no es visual sino aritmético: el error máximo entre la poligonal y la curva real ocurre donde la curvatura es mayor.
Para una curva suave sin oscilaciones, entre 10 y 16 puntos bastan. Para un muelle con dos o tres oscilaciones, hacen falta entre 30 y 60. Para uno muy oscilante, más de 100, y ahí conviene replantearse si es la técnica correcta, porque la cadena de CSS resultante ocupa varios kilobytes.
Un criterio operativo que funciona: cada oscilación completa necesita al menos ocho puntos para no verse angulosa. Cuenta las oscilaciones de la curva que quieres y multiplica.
La forma correcta de repartirlos no siempre es uniforme. En una curva con mucha actividad al principio y una larga cola plana —el caso típico de un muelle amortiguado— la mitad de los puntos uniformes se gastan describiendo una recta. Repartirlos según la curvatura, poniendo más donde el valor cambia deprisa, permite bajar el número total a la mitad para la misma precisión.
El uso obvio de linear() es escribir curvas de rebote a mano, y es el menos interesante. Lo que la función abre de verdad es un cambio de arquitectura: cualquier función matemática del tiempo se puede compilar a CSS. Si puedes evaluarla en JavaScript, puedes muestrearla y emitir una cadena de linear(), y a partir de ahí el movimiento lo ejecuta el motor sin que tu código intervenga en ningún fotograma. Eso mueve la frontera entre lo que necesita JavaScript en ejecución y lo que no, y la mueve mucho: un muelle amortiguado, una curva de aceleración medida de un dispositivo real, una interpolación con corrección perceptual, un perfil sacado de una tabla de un diseñador. Todo eso pasa de ser código que corre sesenta veces por segundo a ser una cadena de texto que se genera una vez, idealmente en tiempo de compilación. Y hay dos consecuencias que compensan el trabajo. La primera es de rendimiento y ya la conoces del nivel del compositor: una animación descrita con fotogramas clave y una función de suavizado puede delegarse al compositor, mientras que la misma trayectoria calculada en un bucle de requestAnimationFrame no puede, por muy transform que sea la propiedad. Convertir tu física a linear() no es solo quitar código: es cambiar de hilo. La segunda es de arquitectura: el resultado es un valor, no un comportamiento, así que se puede guardar en una custom property, versionar en un fichero de tokens, revisar en un diff y compartir entre proyectos sin arrastrar dependencias. Un sistema de movimiento serio en 2026 tiene sus curvas generadas y comprobadas en el repositorio, no calculadas en el navegador de cada usuario.
El compilador es corto y sirve para cualquier función:
// Convierte una funcion f(t) definida en [0,1] en una cadena linear().
// Los extremos se fuerzan a 0 y 1 para evitar errores de redondeo.
function aLinear(f, puntos = 24, decimales = 4) {
const r = (v) => Number(v.toFixed(decimales));
const valores = [];
for (let i = 0; i < puntos; i++) {
const t = i / (puntos - 1);
valores.push(i === 0 ? 0 : i === puntos - 1 ? 1 : r(f(t)));
}
return `linear(${valores.join(', ')})`;
}
// Ejemplo: una curva con dos inflexiones, imposible con cubic-bezier.
const dobleInflexion = (t) => t + 0.18 * Math.sin(2 * Math.PI * t);
console.log(aLinear(dobleInflexion, 20));
Con reparto adaptativo, para gastar puntos donde hacen falta:
// Reparte los puntos segun la variacion de f, no uniformemente.
function aLinearAdaptativo(f, puntos = 20, decimales = 4) {
const N = 400;
// Longitud acumulada de la poligonal fina.
const acum = [0];
for (let i = 1; i <= N; i++) {
const t0 = (i - 1) / N, t1 = i / N;
const d = Math.hypot(t1 - t0, f(t1) - f(t0));
acum.push(acum[i - 1] + d);
}
const total = acum[N];
const salida = [];
for (let k = 0; k < puntos; k++) {
const objetivo = (total * k) / (puntos - 1);
let i = acum.findIndex((v) => v >= objetivo);
if (i < 0) i = N;
const t = i / N;
const y = k === 0 ? 0 : k === puntos - 1 ? 1 : Number(f(t).toFixed(decimales));
const pos = Number((t * 100).toFixed(2));
salida.push(k === 0 || k === puntos - 1 ? `${y}` : `${y} ${pos}%`);
}
return `linear(${salida.join(', ')})`;
}
La versión adaptativa emite las posiciones explícitas, así que los puntos pueden estar donde hagan falta en lugar de repartidos.
Soporte y estrategia
linear() está en los cuatro motores desde finales de 2023: Chrome y Edge 113, Firefox 112, Safari 17.2. Es una capacidad que se puede usar sin plan alternativo en la mayoría de los proyectos.
Si aun así necesitas un plan, el modo de fallo es benigno: un motor que no entiende linear() descarta la declaración entera de transition-timing-function o de animation-timing-function y usa el valor anterior. Así que la técnica de las dos declaraciones consecutivas funciona sin @supports:
.elemento {
animation-timing-function: cubic-bezier(0.34, 1.16, 0.64, 1); /* base */
animation-timing-function: linear(0, 0.42, 0.98, 1.12, 1.02, 1); /* mejora */
}
- Elige una función que oscile, por ejemplo una senoidal amortiguada, y compílala con
aLinearusando 12, 24 y 48 puntos. - Aplica las tres a la misma animación y determina en qué número de puntos deja de notarse la diferencia.
- Repite con la versión adaptativa y comprueba con cuántos puntos consigues la misma calidad.
- Mide el tamaño en bytes de cada cadena y decide cuál llevarías a producción.