Search code examples
csssvghoversvg-animateradial-gradients

Radial SVG gradient is not animating on hover


The radial gradient is supposed to grow from 10% to 100% on hover, but it's just not doing anything. Can't understand what I'm doing wrong.

<svg id="svgDoc" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 500 500" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">

<style><![CDATA[
#svgDoc {
    pointer-events: all
}

#svgDoc:hover #svgGrad {
    animation: svgGrad_f_p 3000ms linear 1 normal forwards
}

@keyframes svgGrad_f_p {
    0% {
        offset: 10%
    } 
    100% {
        offset: 100%
    }
}
]]>
</style>

<defs>
<radialGradient id="svgGrad-fill" cx="0" cy="0" r="0.5" spreadMethod="pad" gradientUnits="objectBoundingBox" gradientTransform="translate(0.5 0.5)">
<stop id="svgGrad-fill-0" offset="0%" stop-color="#ff0000"/>
<stop id="svgGrad-fill-1" offset="10%" stop-color="#000"/>
</radialGradient>
</defs>

<rect id="svgGrad" width="500" height="500" rx="0" ry="0" fill="url(#svgGrad-fill)"/>
</svg>


Solution

  • You can't manipulate the stop attribute via CSS as it "collides" with the CSS offset-path related property of the same name.

    Workaround 1: SMIL animations triggered by begin attribute

    A workaround might be to animate the gradient via SMIL animation like so:

    <h3>Hover me</h3>
    <svg id="svgDoc" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
      <defs>
        <radialGradient id="svgGrad-fill" cx="0" cy="0" r="0.5" spreadMethod="pad" gradientUnits="objectBoundingBox" gradientTransform="translate(0.5 0.5)">
          <stop id="svgGrad-fill-0" offset="0%" stop-color="#ff0000" />
          <stop id="svgGrad-fill-1" offset="10%" stop-color="#000" >
               <animate attributeName="offset" fill="freeze" values="0.1;1" dur="1s" repeatCount="1" begin="svgGrad.mouseover"  /> 
               <animate attributeName="offset" fill="freeze" values="1;0.1" dur="1s" repeatCount="1" begin="svgGrad.mouseout"  /> 
          </stop>
        </radialGradient>
      </defs>
    
      <rect id="svgGrad" width="500" height="500" rx="0" ry="0" fill="url(#svgGrad-fill)" />
    </svg>

    Fortunately, SVG's <animate> element allows you to add events to start (or stop) playback. See "mdn docs: begin"

    <animate attributeName="offset" fill="freeze" values="1;0.1" dur="1s" repeatCount="1" begin="svgGrad.mouseout"  /> 
    

    Adding transition easing

    We need to calcMode="spline" and keySplines attributes to define a custom easing – similar to CSS easing:

    • Ease-in: keySplines="0.42 0 1 1"
    • Ease-in-out: keySplines="0.42 0 0.58 1"
    • Ease: keySplines="0.25 0.1 0.25 1"

    Like the keyTimes attribute we can specify multiple values as an array separated by semicolons.
    In your case we only have 2 animation states (begin:0.1/10% end: 1/100%) as descibed by the keyTimes attribute. Therefore we only need a single bézier value. Otherwise we need to repeat the keySplines according to the keyTimes where keySplines=keyTimes .length-1.

    Here's an example comparing the easing results

    body{
    font-size:3vmin;
    display:grid;
    grid-template-columns: 1fr 1fr 1fr 1fr;
    gap:1em;
    }
    <div class="col">
    <h3>linear</h3>
    <svg viewBox="0 0 500 500">
      <defs>
        <radialGradient id="svgGrad-fill" cx="50%" cy="50%" r="0.5" spreadMethod="pad" gradientUnits="objectBoundingBox">
          <stop offset="0%" stop-color="#ff0000" />
          <stop offset="10%" stop-color="#000">
            
           <animate attributeName="offset" fill="freeze" values="0.1;1;0.1" keyTimes="0;0.5;1"  dur="1s" repeatCount="indefinite" begin="0" />
    
            <!--
            <animate attributeName="offset" fill="freeze" values="0.1;1" keyTimes="0;1" dur="1s" repeatCount="1" begin="svgGrad.mouseover" />
            <animate attributeName="offset" fill="freeze" values="1;0.1" keyTimes="0;1" dur="1s" repeatCount="1" begin="svgGrad.mouseout" />
    -->
          </stop>
        </radialGradient>
      </defs>
    
      <rect id="svgGrad" width="500" height="500" rx="0" ry="0" fill="url(#svgGrad-fill)" />
    </svg>
    </div>
    
    
    <div class="col">
    <h3>Ease-in-out</h3>
    <svg viewBox="0 0 500 500">
      <defs>
        <radialGradient id="svgGradFillEaseInOut" cx="50%" cy="50%" r="0.5" spreadMethod="pad" gradientUnits="objectBoundingBox">
          <stop offset="0%" stop-color="#ff0000" />
          <stop offset="10%" stop-color="#000">
            
            <animate attributeName="offset" fill="freeze" values="0.1;1;0.1" keyTimes="0;0.5;1" calcMode="spline" keySplines="0.42 0 0.58 1; 0.42 0 0.58 1" dur="1s" repeatCount="indefinite" begin="0" />
            <!--
            <animate attributeName="offset" fill="freeze" values="0.1;1" keyTimes="0;1" calcMode="spline" keySplines="0.42 0 0.58 1" dur="1s" repeatCount="1" begin="svgGradEaseInOut.mouseover" />
            <animate attributeName="offset" fill="freeze" values="1;0.1" keyTimes="0;1" calcMode="spline" keySplines="0.42 0 0.58 1" dur="1s" repeatCount="1" begin="svgGradEaseInOut.mouseout" />
    -->
          </stop>
        </radialGradient>
      </defs>
      <rect id="svgGradEaseInOut" width="500" height="500" rx="0" ry="0" fill="url(#svgGradFillEaseInOut)" />
    </svg>
    </div>
    
    
    <div class="col">
    <h3>Ease-in</h3>
    <svg viewBox="0 0 500 500">
      <defs>
        <radialGradient id="svgGradFillEaseIn" cx="50%" cy="50%" r="0.5" spreadMethod="pad" gradientUnits="objectBoundingBox">
          <stop offset="0%" stop-color="#ff0000" />
          <stop offset="10%" stop-color="#000">
            <animate attributeName="offset" fill="freeze" values="0.1;1;0.1" keyTimes="0;0.5;1" calcMode="spline" keySplines="0.42 0 1 1; 0.42 0 1 1" dur="1s" repeatCount="indefinite" begin="0" />
    
            <!--
            <animate attributeName="offset" fill="freeze" values="0.1;1" keyTimes="0;1" calcMode="spline" keySplines="0.42 0 1 1" dur="1s" repeatCount="1" begin="svgGradEaseIn.mouseover" />
            <animate attributeName="offset" fill="freeze" values="1;0.1" keyTimes="0;1" calcMode="spline" keySplines="0.42 0 1 1" dur="1s" repeatCount="1" begin="svgGradEaseIn.mouseout" />
    -->
          </stop>
        </radialGradient>
      </defs>
      <rect id="svgGradEaseIn" width="500" height="500" rx="0" ry="0" fill="url(#svgGradFillEaseIn)" />
    </svg>
    </div>
    
    <div class="col">
    <h3>Ease</h3>
    <svg viewBox="0 0 500 500">
      <defs>
        <radialGradient id="svgGradFillEase" cx="50%" cy="50%" r="0.5" spreadMethod="pad" gradientUnits="objectBoundingBox">
          <stop offset="0%" stop-color="#ff0000" />
          <stop offset="10%" stop-color="#000">
            
            <animate attributeName="offset" fill="freeze" values="0.1;1;0.1" keyTimes="0;0.5;1" calcMode="spline" keySplines="0.25 0.1 0.25 1; 0.25 0.1 0.25 1" dur="1s" repeatCount="indefinite" begin="0" />
    
            
            <!--
            <animate attributeName="offset" fill="freeze" values="0.1;1" keyTimes="0;1" calcMode="spline" keySplines="0.25 0.1 0.25 1" dur="1s" repeatCount="1" begin="svgGradEase.mouseover" />
            <animate attributeName="offset" fill="freeze" values="1;0.1" keyTimes="0;1" calcMode="spline" keySplines="0.25 0.1 0.25 1" dur="1s" repeatCount="1" begin="svgGradEase.mouseout" />
    -->
          </stop>
        </radialGradient>
      </defs>
      <rect id="svgGradEase" width="500" height="500" rx="0" ry="0" fill="url(#svgGradFillEase)" />
    </svg>
    </div>

    Workaround 2: CSS custom @property

    By defining custom properties for start and end offsets we can also achieve a smooth gradient transition.

    @property --offset1 {
      syntax: "<percentage>";
      inherits: false;
      initial-value: 0%;
    }
    
    
    @property --offset2 {
      syntax: "<percentage>";
      inherits: false;
      initial-value: 10%;
    }
    
    
    .gradient{
      background: radial-gradient( #FF0000 var(--offset1), #000 var(--offset2));
      transition:1s --offset1, 1s --offset2;
    }
    
    .gradient:hover{
      --offset1: 5%;
      --offset2:100%;
    }
    <h3>Gradient CSS</h3>
    <svg id="svgDoc2" class="gradient" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
    </svg>

    This won't work with CSS variables as we can't transition the background property itself.

    In this approach we're applying a CSS gradient to the outermost (parent) SVG element – accepting CSS gradient styles just like a HTML element (unlike inner SVG elements).

    However, the SMIL approach probably still offers the best cross-browser compatibility.

    See also: