Styling with CSS and paint servers

Presentation attributes versus CSS rules, fill and stroke semantics, gradients and markers, and transforms that respect the element's own box.

Attributes or CSS?

<svg viewBox="0 0 200 100" class="chart">
  <polyline class="series" points="0,80 50,40 100,60 150,10 200,30" />
</svg>

<style>
  .chart { overflow: visible; }
  .series {
    fill: none;
    stroke: currentColor;      /* inherits the CSS color of the element */
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;  /* keeps 2px at any scale */
  }
  .chart:hover .series { stroke: #f97316; }
</style>

SVG presentation attributes are CSS properties with the lowest possible priority — any matching CSS rule overrides them. That makes attributes the right place for per-instance data from a template, and CSS the right place for theme, hover and dark-mode styling.

PropertyApplies toNotes
fillClosed shapes, textnone switches it off; currentColor follows CSS text colour
stroke / stroke-widthAny shape or pathStroke is centred on the outline and can be clipped by the viewport
stroke-dasharrayStrokesDash pattern; also the basis of the draw-on animation
opacityAny elementGroup opacity composites the group, unlike per-shape opacity
fill-rulenonzero or evenoddDecides which regions of a self-overlapping path are filled
paint-orderTextstroke fill keeps a text outline from cutting into the glyphs

Gradients, patterns and markers

<svg viewBox="0 0 200 100">
  <defs>
    <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%"   stop-color="#93c5fd" />
      <stop offset="100%" stop-color="#1d4ed8" />
    </linearGradient>
    <pattern id="hatch" width="8" height="8" patternUnits="userSpaceOnUse">
      <path d="M0 8 L8 0" stroke="#94a3b8" stroke-width="1" />
    </pattern>
    <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
            markerWidth="6" markerHeight="6" orient="auto-start-reverse">
      <path d="M0 0 L10 5 L0 10 z" fill="#0f172a" />
    </marker>
  </defs>

  <rect x="10" y="10" width="180" height="50" fill="url(#sky)" />
  <rect x="10" y="70" width="180" height="20" fill="url(#hatch)" />
  <line x1="10" y1="55" x2="170" y2="55" stroke="#0f172a"
        stroke-width="2" marker-end="url(#arrow)" />
</svg>
  • gradientUnits defaults to objectBoundingBox, so the gradient stretches to each element it paints; userSpaceOnUse shares one gradient across shapes.
  • A marker with orient="auto-start-reverse" can be used for both the start and end of a line.
  • Patterns tile in user space — a stroke-heavy pattern on a large area is a rendering cost, not a free texture.

Transforms, filters and clipping

<style>
  .rot { transform: rotate(15deg); transform-box: fill-box; transform-origin: center; }
  .soft { filter: url(#blur); }
</style>

<svg viewBox="0 0 200 120">
  <defs>
    <filter id="blur" x="-20%" y="-20%" width="140%" height="140%">
      <feGaussianBlur stdDeviation="3" />
    </filter>
    <clipPath id="half"><rect x="0" y="0" width="100" height="120" /></clipPath>
  </defs>

  <rect class="rot" x="20" y="20" width="60" height="60" fill="#a5b4fc" />
  <circle class="soft" cx="140" cy="60" r="35" fill="#fbbf24" />
</svg>
ConceptSyntaxRemember
Transformtransform="translate(10 5) rotate(30)"Space-separated transforms, no commas between functions
CSS transform origintransform-box: fill-boxWithout it, percentages resolve against the viewport, not the shape
Filter regionx y width height on filterDefaults to -10%/+120%; blur and drop shadow get clipped without a larger region
Clip pathclip-path="url(#id)"Hard edge, no partial transparency
Maskmask="url(#id)"Luminance-based, so it fades — and it is more expensive than clipping
⚠️
transform-box: fill-box is what makes transform-origin: center mean the shape's own centre. In older WebKit builds this is not honoured, so if a rotation looks anchored to the wrong point, animate the transform attribute instead of the CSS property.

FAQ

Why is my CSS not applying to the SVG?
External stylesheet rules do not cross into an SVG referenced by <img> or background-image, because it is a separate document with no scripting or styling context. Inline the SVG when you need CSS to reach the shapes.
How do I get crisp 1px lines?
Use vector-effect: non-scaling-stroke together with a half-pixel offset, such as y="10.5" for a horizontal line, so the stroke lands on the pixel grid instead of straddling two rows.

Shapes and coordinates Selectors and specificity

Last refreshed 2026-09-18.