Dibujar el trazo: dasharray, dashoffset y la longitud
La matemática exacta del efecto de dibujado, cómo obtener la longitud con getTotalLength, y el truco de pathLength que lo resuelve sin JavaScript.
El efecto de un trazo que se dibuja solo es probablemente la animación de SVG más usada, y es también la que más gente implementa copiando sin entender. Detrás hay una idea de dos líneas: convertir el trazo en un patrón discontinuo cuyo primer segmento mide exactamente lo que el trazado entero, y desplazar ese patrón. Entender la aritmética permite construir todas las variantes —dibujado inverso, dibujado parcial, dibujado por tramos— en lugar de solo la básica.
- Explicar por qué el par dasharray y dashoffset produce un dibujado.
- Obtener la longitud de un trazado con
getTotalLength(). - Usar
pathLengthpara conseguir el mismo efecto sin JavaScript. - Construir variantes del efecto sobre la misma base.
La aritmética
stroke-dasharray define un patrón de guiones y huecos. stroke-dasharray: 10 5 dibuja diez unidades de trazo, cinco de hueco, y repite. stroke-dashoffset desplaza el punto de inicio de ese patrón a lo largo del trazado.
El truco del dibujado consiste en construir un patrón degenerado: un único guion tan largo como el trazado entero, seguido de un hueco igual de largo. Si la longitud total es L:
stroke-dasharray: Lsignifica un guion deLy un hueco deL, porque un valor único se repite para el hueco.stroke-dashoffset: Ldesplaza el patrónLunidades, con lo que el guion queda completamente fuera y lo que se ve en todo el trazado es el hueco. El trazo es invisible.stroke-dashoffset: 0deja el guion en su sitio y el trazo se ve entero.
Animar el desplazamiento de L a 0 hace que el guion entre progresivamente por el principio del trazado. Eso es el dibujado, y no hay nada más.
De ahí salen las variantes de forma inmediata:
- De
0aLen lugar de al revés: el trazo se borra desde el principio. - De
-La0: el trazo se dibuja desde el otro extremo, porque el desplazamiento negativo mueve el patrón en sentido contrario. - De
LaL/2: se dibuja la mitad del trazado.
La longitud, desde el DOM
El único dato que falta es L, y no hay forma de obtenerlo en CSS: hay que preguntárselo al elemento. Las formas geométricas de SVG implementan getTotalLength(), que devuelve la longitud del trazado en unidades de usuario.
document.querySelectorAll('.dibujar').forEach((forma) => {
const l = forma.getTotalLength();
forma.style.setProperty('--longitud', l);
});
.dibujar {
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-dasharray: var(--longitud);
stroke-dashoffset: var(--longitud);
animation: dibujar 1.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes dibujar {
to { stroke-dashoffset: 0; }
}
Ese reparto —JavaScript solo para medir, CSS para animar— es el correcto y merece señalarse: la animación sigue siendo declarativa, respeta prefers-reduced-motion con una media query, aparece en getAnimations() y se puede pausar con las herramientas del navegador. Lo único que hace el JavaScript es aportar un número que CSS no puede calcular.
Un detalle de la medición: getTotalLength() devuelve la longitud del trazado tal y como está definido, en unidades de usuario, sin tener en cuenta las transformaciones. Si tu forma tiene un transform: scale(2), la longitud medida no es la que se ve en pantalla. Para el efecto de dibujado eso da igual, porque stroke-dasharray también se mide en unidades de usuario y las dos escalan juntas. Solo importa si estás usando la longitud para otra cosa.
Sin JavaScript, con pathLength
Hay una salida elegante que casi nadie conoce y que elimina la medición por completo. El atributo pathLength le dice al navegador qué longitud debe fingir que tiene el trazado a efectos de todos los cálculos de trazo. Si escribes pathLength="1", el motor escala internamente todo el sistema de longitudes para que el trazado mida uno.
<svg viewBox="0 0 100 100">
<path class="dibujar" pathLength="1"
d="M 10 50 Q 50 10 90 50 T 170 50" />
</svg>
.dibujar {
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: dibujar 1.4s ease-out forwards;
}
@keyframes dibujar {
to { stroke-dashoffset: 0; }
}
Con pathLength="1", la longitud es siempre uno, sea cual sea la forma. El CSS deja de depender de ninguna medida y el mismo bloque de reglas sirve para cualquier trazado, sin JavaScript, sin variables y sin medir nada. Es la versión que conviene usar por defecto, y la razón de que no sea más conocida es que los tutoriales antiguos son anteriores a que el atributo tuviera buen soporte.
Con pathLength="100" en lugar de uno, el mismo mecanismo permite razonar en porcentajes: stroke-dashoffset: 40 es “faltan el cuarenta por ciento”, lo cual se lee mejor cuando quieres dibujados parciales.
Conviene tener el modelo mental correcto porque predice todos los casos en que el truco no funciona. Lo que estás haciendo no es dibujar progresivamente: es recortar un trazo que ya está entero, moviendo un patrón de guiones. De ahí se deducen sus tres límites sin tener que descubrirlos a base de intentos. Primero: solo funciona sobre stroke, nunca sobre fill. Una forma rellena no tiene patrón de guiones que desplazar, así que un logotipo hecho de siluetas rellenas no se puede “dibujar” con esta técnica; hay que convertirlo a trazados con trazo, y eso es trabajo de la herramienta de diseño, no del CSS. Segundo: el orden del dibujado es el orden de los comandos del trazado, no el orden visual. Si el trazado se definió empezando por la mitad, el dibujado empieza por la mitad, y no hay ninguna propiedad que lo arregle: hay que reordenar la d. Es la causa del noventa por ciento de los “se dibuja en un orden raro”. Tercero: un elemento path con varios subtrazados se dibuja como uno solo continuo, porque la longitud total suma todos los subtrazados y el patrón los recorre en secuencia. Si quieres que las tres letras de un logotipo se dibujen a la vez, tienen que ser tres elementos, no uno con tres subtrazados. Y una consecuencia positiva que se aprovecha poco: como el efecto es un recorte, es perfectamente reversible y perfectamente interpolable, así que se puede atar a una timeline de scroll o a un progreso arbitrario sin ninguna consideración especial. Un indicador circular de progreso es exactamente esta técnica con el desplazamiento calculado a partir de un porcentaje.
Un indicador de progreso completo
Cerrando con el caso que reúne todo, un anillo de progreso sin medir nada y sin JavaScript salvo para el valor:
<svg class="anillo" viewBox="0 0 40 40" style="--progreso: 62">
<circle class="anillo__fondo" cx="20" cy="20" r="16" pathLength="100" />
<circle class="anillo__valor" cx="20" cy="20" r="16" pathLength="100" />
</svg>
.anillo {
inline-size: 5rem;
transform: rotate(-90deg); /* que empiece arriba */
}
.anillo circle {
fill: none;
stroke-width: 4;
stroke-linecap: round;
}
.anillo__fondo {
stroke: oklch(90% 0.01 260);
}
.anillo__valor {
stroke: oklch(60% 0.18 250);
stroke-dasharray: 100;
stroke-dashoffset: calc(100 - var(--progreso));
transition: stroke-dashoffset 400ms cubic-bezier(0.2, 0, 0, 1);
}
@media (prefers-reduced-motion: reduce) {
.anillo__valor { transition-duration: 1ms; }
}
Con pathLength="100", el desplazamiento es literalmente el porcentaje que falta, y actualizar el progreso es escribir una custom property. La transición se encarga del movimiento. No hay medición, no hay @keyframes y no hay una sola línea de JavaScript de animación.
Coge un trazado con tres subtrazados —tres letras, por ejemplo— y consigue que se dibujen uno detrás de otro con un pequeño solape, sin separarlos en tres elementos. Pista: con pathLength="1" cada subtrazado ocupa una fracción conocida del total si mides sus longitudes relativas, y un patrón de stroke-dasharray con cuatro valores puede describir tres guiones y tres huecos. Cuando lo tengas, compáralo con la versión de tres elementos y decide cuál mantendrías.