wandres.dev
ESTILAR SVG · Presentación, CSS y herencia

Qué propiedades SVG viven en CSS

El inventario real de propiedades de pintura, trazo, texto y geometría que se pueden escribir desde una hoja de estilos, y la trampa de transform-box en las transformaciones.

⏱ 18 min

La pregunta «puedo poner esto en CSS» tiene una respuesta binaria por atributo y casi nadie tiene la lista en la cabeza. Saberla ahorra horas, porque la mitad de los intentos fallidos de estilar un SVG son intentos de escribir desde CSS algo que nunca ha sido una propiedad. Y dentro de lo que sí lo es hay una trampa mayor que todas las demás juntas: transform funciona, pero su origen no está donde crees.

🎯 Al terminar esta lección sabrás
  • Enumerar las propiedades de pintura, trazo y texto disponibles desde CSS.
  • Distinguir un atributo de presentación de un atributo geométrico o de configuración.
  • Corregir el origen de una rotación en SVG con transform-box.
  • Usar las propiedades de geometría y saber qué límite tienen.

Pintura y trazo

Este es el bloque grande y el que se usa a diario. Todas son propiedades CSS de pleno derecho, todas se heredan salvo donde se indique, y todas admiten var(), transition y @media.

Propiedad Valores Nota
fill color, url(#id), none, currentColor Hereda
fill-opacity 0 a 1, porcentaje Hereda
fill-rule nonzero | evenodd Hereda
stroke igual que fill Hereda
stroke-width longitud, porcentaje Hereda
stroke-opacity 0 a 1 Hereda
stroke-linecap butt | round | square Hereda
stroke-linejoin miter | round | bevel Hereda
stroke-miterlimit número, mínimo 1 Hereda, inicial 4
stroke-dasharray lista de longitudes, none Hereda
stroke-dashoffset longitud Hereda
paint-order permutación de fill stroke markers Hereda
vector-effect none | non-scaling-stroke No hereda
marker-start url(#id) | none Hereda
marker-mid url(#id) | none Hereda
marker-end url(#id) | none Hereda
color-interpolation-filters sRGB | linearRGB Hereda, decisiva en filtros
shape-rendering auto | optimizeSpeed | crispEdges | geometricPrecision Hereda

Tres apuntes prácticos sobre esta tabla.

stroke-dasharray acepta una lista con espacios o comas, y si la longitud de la lista es impar se duplica: stroke-dasharray: 5 equivale a 5 5, y 5 3 2 equivale a 5 3 2 5 3 2. Es una regla heredada de PostScript que sorprende la primera vez.

vector-effect: non-scaling-stroke es la propiedad que hace posible una rejilla de gráfico que no engorde al escalar el viewBox. No se hereda, así que hay que ponerla en cada elemento o usar un selector.

color-interpolation-filters no parece de este bloque y es la propiedad más importante que existe para los filtros, porque su valor inicial es linearRGB y eso hace que todo filtro escrito sin pensar salga con un aspecto distinto del esperado. Se trata en el nivel 22.

Texto y disposición

Propiedad Nota
text-anchor start | middle | end, hereda
dominant-baseline La línea base a la que se alinea, hereda
alignment-baseline Sobre un tspan respecto a su padre
letter-spacing, word-spacing Las mismas de CSS, funcionan
font-* completo font-family, font-size, font-weight, etc.
writing-mode Vertical, con soporte desigual en SVG
direction, unicode-bidi Texto de derecha a izquierda

Y las de composición y efectos, que no heredan ninguna: opacity, clip-path, clip-rule (esta sí hereda), mask, filter, mix-blend-mode, isolation, display, visibility, pointer-events, cursor, overflow.

Las de gradientes y filtros: stop-color, stop-opacity, flood-color, flood-opacity, lighting-color. Todas son propiedades, lo que significa que puedes tematizar un gradiente desde CSS sin tocar el marcado, y eso es más útil de lo que parece.

:root { --acento: #89b4fa; }
[data-tema="calido"] { --acento: #fab387; }

#gradiente stop:first-child { stop-color: var(--acento); }
#gradiente stop:last-child  { stop-color: transparent; }

Lo que nunca ha sido una propiedad

Esta lista es igual de importante y más corta:

  • viewBox, preserveAspectRatio
  • d (en los motores donde existe como propiedad, no está en los tres; no dependas de ello)
  • points
  • x1, y1, x2, y2 de un gradiente; cx, cy, r, fx, fy, fr de un radial
  • gradientUnits, gradientTransform, spreadMethod
  • patternUnits, patternContentUnits, patternTransform
  • clipPathUnits, maskUnits, maskContentUnits
  • filterUnits, primitiveUnits, y todos los atributos de las primitivas (stdDeviation, in, result, values…)
  • refX, refY, markerWidth, markerHeight, orient, markerUnits
  • offset de un stop
  • href, startOffset, textLength, lengthAdjust

Si tu tema necesita cambiar cualquiera de estos, la respuesta no es CSS: es generar marcado distinto o escribir el atributo desde JavaScript. Conviene saberlo antes de diseñar el sistema, no después.

Las propiedades de geometría

SVG 2 promovió a propiedades CSS los atributos geométricos de las formas: cx, cy, r, rx, ry, x, y, width, height. Con ellas puedes escribir esto:

.punto { r: 4; transition: r 150ms ease; }
.punto:hover { r: 7; }

Es una mejora real: una transición de radio sin JavaScript, con el interpolador del navegador. Firefox fue el último motor en implementarlas y hoy están disponibles en los tres, pero conviene tenerlo presente si tu matriz de soporte incluye versiones antiguas; la degradación es benigna, porque el atributo sigue funcionando y solo se pierde la transición.

Dos límites que no son de soporte sino de diseño. El primero: solo se aplican a los elementos que tienen esos atributos. Un path no tiene width. El segundo: cuando el valor viene de datos, sigue haciendo falta escribir el atributo desde JavaScript, así que las propiedades de geometría son para estados de interacción, no para el dato en sí.

transform, y el origen que no está donde crees

transform es una propiedad CSS y funciona sobre elementos SVG. También lo son translate, rotate y scale como propiedades independientes. Y aquí está la trampa que cuesta más tiempo de todo el nivel.

En HTML, transform-origin: 50% 50% significa el centro del propio elemento, porque transform-box vale border-box para las cajas. En SVG, el valor inicial de transform-box es view-box, es decir, el sistema de coordenadas del viewport SVG. Un transform-origin: 50% 50% sobre un circle no es el centro del círculo: es el centro del lienzo entero.

<svg viewBox="0 0 200 100" width="400">
  <rect class="mal"  x="20"  y="30" width="50" height="40" fill="#f38ba8" />
  <rect class="bien" x="120" y="30" width="50" height="40" fill="#a6e3a1" />
</svg>
.mal, .bien {
  transform-origin: 50% 50%;
  animation: girar 3s linear infinite;
}
/* La correccion: que el origen sea la caja del propio elemento */
.bien { transform-box: fill-box; }

@keyframes girar { to { transform: rotate(360deg); } }

El rojo describe una órbita alrededor del centro del lienzo. El verde gira sobre sí mismo. La única diferencia es transform-box: fill-box.

fill-box, view-box y el atributo transform no son intercambiables

Hay tres formas de transformar un elemento SVG y las tres tienen orígenes distintos por omisión, lo que produce discrepancias que parecen aleatorias.

El atributo transform="rotate(45)" usa siempre el origen del sistema de coordenadas del usuario, es decir (0, 0) del viewBox, y admite un origen explícito con la sintaxis de tres argumentos: rotate(45 60 40). No lee transform-origin ni transform-box.

La propiedad transform: rotate(45deg) sí los lee, y con transform-box: view-box (el valor inicial) el porcentaje se resuelve contra el viewport SVG.

Con transform-box: fill-box el porcentaje se resuelve contra la caja envolvente del propio elemento, que es la que devuelve getBBox(). Y aquí está el matiz que cierra el círculo con el nivel anterior: esa caja no incluye el trazo. Un elemento con un stroke-width grande rota alrededor del centro de su geometría, no del centro de lo que se ve. En un icono de línea gruesa la diferencia es de un píxel o dos y se percibe como un bamboleo.

Y hay un caso más traicionero: sobre un elemento con caja envolvente de altura cero (una línea horizontal), fill-box da un origen degenerado y el resultado depende del motor. En esos casos el atributo con origen explícito sigue siendo la opción robusta.

⚔️ Reto práctico

Construye un icono de carga giratorio con dos anillos concéntricos que rotan en sentidos opuestos, usando solo CSS. Comprueba primero cómo se comporta sin transform-box, después con fill-box, y por último con el atributo transform con origen explícito. Mide cuál de las tres versiones sigue centrada cuando cambias el viewBox del SVG sin tocar nada más.