<h1>CSS Hover Effects, Transitions and SVG <code><use></code> elements</h1>
<p>Browsers get buggy when applying hover effects to a graphic which is duplicated using the SVG <code><use></code> element. Things get really wonky if you add in CSS transitions.<p>
<figure>
<svg>
<g id="myGroup">
<rect id="myRect" width="80%" height="40%"
x="10%" y="5%" />
<text y="50%" dy="0.5em">Original at top left</text>
<use xlink:href="#myRect" y="50%" />
</g>
</svg>
<svg>
<use xlink:href="#myGroup"/>
</svg>
<figureCaption>The graphic shows two separate SVG elements. The one on the left consists of a <code><g></code> (group) element, containing a <code><rect></code> element (top rectangle), and a <code><use></code> element referencing it (bottom rectangle). The second SVG (on the right) contains a <code><use></code> element referencing the group from the first SVG. </figureCaption>
</figure>
<h2>Simple hover effects</h2>
<p>There are two non-transitioned hover-rules in effect. If you hover anywhere on the SVG containing the original rectangle, the rectangle should change stroke (outline) colour:<code>svg:hover #myRect { stroke:blue; }</code>. If you hover the rectangle itself, it should change fill colour: <code>#myRect:hover { fill:magenta; }</code>.</p>
<ul>
<li><p>Webkit browsers (tested on Chrome 33 and Opera 20):
<p>The hover effect on the rectangle itself <em>appears</em> to only affect that rectangle, not the duplicates, but the changes show up on some or all of the <code><use></code> elements if a repaint gets triggered while the original is in its hovered state (move the mouse around quickly to make it happen). If a duplicate's styles get switched, they will stay in the hover state until another repaint gets triggered by other changes.
<p>The hover effect on the SVG affects all instances of the rectangle <em>within the original SVG</em>, when that SVG is moused-over.</p>
<p><em>Update for Chrome 34:</em> The repaints of all the <code><use></code> elements now get triggered automatically when the source element is hovered. However, the CSS transition repaints (see below) are still buggy.</p>
</li>
<li><p>Firefox (tested in 27 & 28):
<p>The hover effect on the rectangle is applied to whichever element (original or duplicate) is hovered over, and no others.
<p>The hover effect on the SVG applies to the rectangles within a given SVG, when that SVG is moused-over.</li>
<li><p>Internet Explorer (tested in 11, including 9 and 10 emulation modes):
<p>The hover effect on the rectangle is applied to <em>all</em> instances <em>within the same SVG</em> when the <em>original</em> is hovered-over. Duplicates in the second SVG are not immediately repainted, but will sometimes display the alternate styles if a repaint is triggered. Even within the same SVG the duplicates are not immediately re-painted when the hover effect is <em>removed</em>.
<p>The hover effect on the SVG applies to all instances (original and duplicate) within that SVG; but as with the rectangle-hover, the styles in the second SVG will be changed if a repaint is triggered (by rapid mouse movement) before the first SVG has a chance to discard the hover effects.
</ul>
<h2>Transitioned effects</h2>
<p>Further eratic results occur if you try to use CSS transitions on an SVG element that is duplicated using SVG use elements. The change in stroke-width is triggered by hover on the SVG, and has a 3 second transition: <code>rect { transition: stroke-width 3s; } svg:hover #myRect { stroke-width:10; }</code>.</p>
<ul>
<li><p>Webkit browsers:
<p>The <code><use></code> elements do not repaint when the source element style changes, but if a repaint is triggered on one of the duplicates in either SVG (e.g., by mouse-over) it will update to match the original rectangle's style at that point in the transition.</li>
<li><p>Firefox:
<p>All instances in the SVG that is being moused-over repaint with transitioning styles; the hover effect applies on either SVG.</li>
<li><p>Internet Explorer: <p>IE doesn't transition/animate SVG styles, and demonstrates the same behaviour as for non-transitioned hover styles (the change affects all instances, but only elements within the same SVG are repainted by default).
</ul>
<h2>What <em>should</em> happen?</h2>
<p>I don't think anyone would disagree that the behaviour shown by Webkit and IE, with certain style changes only appearing if something else triggers a repaint, is buggy. But the <a href="http://www.w3.org/TR/SVG11/struct.html#UseElement">W3 Specs on the <code><use></code> element</a> aren't exactly crystal clear about expected behaviour.</p>
<p>The general rule for <code><use></code> elements and CSS styles is that the duplicate version gets a copy of any styles declared directly on the referenced graphic or its components, but not any inherited styles (instead, it inherits default values from the <code><use></code> element and its ancestors). Because all the hover styles are being applied directly to the <code>#myRect</code> rectangle (even if they are triggered by a hover on the parent SVG) that shouldn't be an issue.</p>
<p>The specs don't talk about CSS pseudo-classes,although <a href="http://lists.w3.org/Archives/Public/www-svg/2010May/0004.html">this thread from the SVG working group mailing list</a> backs the Firefox approach, with each element getting the hover style applied when it (and it alone) is hovered. </p>
<p>The specs <em>do</em> mention that animations on the original graphic should apply to all copies, and that appears to work fine for SMIL animations on browsers that support them.</p>
<p>The specs also discuss event handling with respect to duplicated elements. An event listener added to an element is also supposed to be added to all duplicate copies of that element. If the event is triggered on a duplicate, the target of that event should be an <code>SVGElementInstance</code> object, part of the shadow DOM of such objects within each <code><use></code> element. You can't set styles directly on an SVGElementInstance, but you can access both the original graphical element it is a copy of (as <code>.correspondingElement</code>) and the <code><use></code> element it is a part of (as <code>.correspondingUseElement</code>).</p>
<p>My expectation was that browser implementation of mouse event handling behaviour would mimic the hover-effect behaviour, but it's quite the opposite.</p>
<h2>Mouse event handling</h2>
<p>The original rectangle has a click event handler added to it (if you're in editor mode, click the blue "Run" button on the JS tab to add the event listener). The event handler tests to see if the target of the event was a <code>SVGElementInstance</code>, and if so accesses both the corresponding graphical element and the corresponding <code><use></code> element. It then temporarily applies a rotation (transform attribute) and dotted border (inline style) to the original graphic and fades the stroke and fill opacity (as attribute and style respectively) of the use element. If the target was the original graphics element all the changes are applied directly to the target element.
</p>
<p>The expected behaviour according to the specifications is that changes to the original element, whether specified as styles or attributes, get applied to all copies, and the changes to the use element get applied to all content within that element and any copies thereof (e.g., if you click the lower left rectangle the copy of it in the right SVG will also be faded out; if you click either rectangle on the right, both will fade out since they are both part of the same <code><use></code> element.</p>
<p>This is <em>almost</em> how it works in webkit and IE10/11 (the IE9 emulator is inconsistent). However, the problem with repaints still shows up if when clicking on the lower left rectangle (a duplicate of the original which is then duplicated itself): the element referencing it in the second SVG doesn't always get repainted correctly.</p>
<p>If you were thinking that Firefox was the browser of choice based on the hover style behaviour, the event handling behaviour might change your mind. Firefox (as of version 28, anyway) <a href="https://developer.mozilla.org/en-US/docs/SVG_in_Firefox">hasn't implemented <code>SVGElementInstance</code></a> (I had to add a check to prevent the event handler from throwing errors when testing the class), and doesn't seem to have any programmatic way of accessing the shadow DOM within a <code><use></code> element. The event handler is added to the original rectangle only, and behaves as expected -- changes are applied to the original and all copies. No events are fired for clicks on the copies.
h1 {text-align:center; border-bottom:solid;}
body {max-width:50em;}
svg {
background:lightgray;
width:45%; height:200px;
margin:2%;
}
rect {
fill:seagreen;
stroke:darkgreen;
stroke-width:2;
transition: stroke-width 3s;
}
#myRect:hover {
fill:magenta;
}
svg:hover #myRect {
stroke:blue;
stroke-width:10;
}
svg:first-of-type text {
fill:currentColor;
font-weight:bold;
transition:8s;
}
svg:nth-of-type(2) {
color:indigo;
transition:color 2s;
}
svg:nth-of-type(2):hover{
color:red;
}
p code, figureCaption code {
display:inline-block;
background:#e0e8ff;
padding:0.2em;
}
li {
margin: 0.5em 0;
}
li p {
margin:0.3em 0;
}
li p:first-of-type {
font-weight:bold;
}
document.getElementById("myRect").addEventListener("click", twist);
function twist(e){
var target = e.target, original;
console.log("Twisting", target, e);
if ((window.SVGElementInstance)&&
(target instanceof SVGElementInstance)){
original = target.correspondingElement;
console.log("...a copy of", original);
target = target.correspondingUseElement;
console.log("...referenced within", target);
}
else {
original = target;
}
original.setAttribute("transform", "rotate(15)");
original.style["strokeDasharray"] = "1 1";
target.setAttribute("stroke-opacity", "0.5");
target.style["fill-opacity"] = "0.5";
setTimeout(function(){untwist(target, original);}, 1500);
}
function untwist(target, original){
original.removeAttribute("transform");
original.style["strokeDasharray"] = "";
target.removeAttribute("stroke-opacity");
target.style["fill-opacity"] = "";
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.