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.
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.
- 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,preserveAspectRatiod(en los motores donde existe como propiedad, no está en los tres; no dependas de ello)pointsx1,y1,x2,y2de un gradiente;cx,cy,r,fx,fy,frde un radialgradientUnits,gradientTransform,spreadMethodpatternUnits,patternContentUnits,patternTransformclipPathUnits,maskUnits,maskContentUnitsfilterUnits,primitiveUnits, y todos los atributos de las primitivas (stdDeviation,in,result,values…)refX,refY,markerWidth,markerHeight,orient,markerUnitsoffsetde unstophref,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.
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.
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.