302

I am using jQuery SVG. I can't add or remove a class to an object. Anyone know my mistake?

The SVG:

<rect class="jimmy" id="p5" x="200" y="200" width="100" height="100" />

The jQuery that won't add the class:

$(".jimmy").click(function() {
    $(this).addClass("clicked");
});

I know the SVG and jQuery are working together fine because I can target the object and fire an alert when it's clicked:

$(".jimmy").click(function() {
    alert('Handler for .click() called.');
});
Alexander O'Mara
  • 58,688
  • 18
  • 163
  • 171
Don P
  • 60,113
  • 114
  • 300
  • 432
  • 4
    Good article here: http://toddmotto.com/hacking-svg-traversing-with-ease-addclass-removeclass-toggleclass-functions/ – Kris Jun 12 '14 at 15:31
  • 8
    So the actual question of *WHY* can't I use the jQuery *class functions remains unanswered... If I can access the properties of SVG elements via jQuery attr function, why can't the *class functions also do this? jQuery FAIL?!? – Ryan Griggs Dec 02 '14 at 02:15
  • 2
    Seriously, does anyone know _WHY_?? – Ben Wilde Feb 20 '15 at 18:33
  • Tiny library that adds this functionality for SVG files: https://github.com/toddmotto/lunar – Chuck Le Butt Apr 08 '15 at 11:07
  • 6
    The "why" is because jQuery uses the "className" property, which is a string property for HTML elements, but is an SVG Animated String for SVG elements. Which can't be assigned to like for HTML elements. Thus, the jQuery code blows up trying to assign the value. – Jon Watte May 20 '16 at 00:04
  • As per https://stackoverflow.com/a/34698009/70345 the correct answer is: upgrade to one of jQuery 1.12.0, 2.2.0, or 3.0; or a later version of these. – Ian Kemp Sep 26 '17 at 13:16

15 Answers15

438

Edit 2016: read the next two answers.

  • JQuery 3 fixes the underlying issue
  • Vanilla JS: element.classList.add('newclass') works in modern browsers

JQuery (less than 3) can't add a class to an SVG.

.attr() works with SVG, so if you want to depend on jQuery:

// Instead of .addClass("newclass")
$("#item").attr("class", "oldclass newclass");
// Instead of .removeClass("newclass")
$("#item").attr("class", "oldclass");

And if you don't want to depend on jQuery:

var element = document.getElementById("item");
// Instead of .addClass("newclass")
element.setAttribute("class", "oldclass newclass");
// Instead of .removeClass("newclass")
element.setAttribute("class", "oldclass");
forresto
  • 12,078
  • 7
  • 45
  • 64
  • In support to forresto suggestion - http://discusscode.blogspot.in/2012/08/talking-smiley-with-svg-and-jquery.html – helloworld Aug 05 '12 at 07:47
  • perfect. this should be the answer. though the REAL answer is for jquery to include this by default, at least as a setting or something. – AwokeKnowing Nov 04 '13 at 23:39
  • 2
    `.attr("class", "oldclass")` (or the more cumbersome `.setAttribute("class", "oldclass")`) is really not equivalent to removing `newclass`! – Tom Jul 29 '14 at 14:36
  • Great answer ( and v.nice technique ) ... but would it be an idea to edit the question so that it starts by stating that jquery can't add a class to an SVG ( if that's the case ... as it seems )? – byronyasgur Dec 15 '14 at 23:33
  • The non-jQuery version won't work on old IE (certainly 7 and earlier, can't remember about 8) because `setAttribute()` is broken. – Tim Down Feb 11 '15 at 11:58
  • Sagar Gala posted much better solution. Manually setting class isn't the same as jquery add/removeClass function. – Oleksandr IY Apr 10 '15 at 08:30
  • The OP asked **"why"**, not **"how"**. so this answer did not answer the question at all. – vsync Aug 21 '15 at 17:46
  • Does anyone know how to preserve the existing classes because they are getting wiped when I add the attribute class? – finners Mar 22 '16 at 12:04
  • @FrazerFindlater `oldclass` in the example is for that `$("#item").attr("class", "oldclass newclass");` – forresto Mar 29 '16 at 01:48
  • 1
    Just an addendum: I tried and JQuery 3 did not fix the issue. – Joao Arruda Sep 08 '16 at 19:50
  • Regarding your 2016 classList edit - please read the comment in Tomas Mikula's answer below. IE does not implement classList in SVG elements. – TimHayes Jan 04 '17 at 18:23
  • a small workaround i ended up with : instead of `$("#item").addClass("newClass")` I'm writing `$("#item").attr("class", $(#item).attr('class').replace('newClass','')+' newClass');` and for `$("#item").removeClass("oldClass");` I'm writing `$("#item").attr("class", $(#item).attr('class').replace('oldClass',''));` in that way, you don't have duplicata of class – Fenix Aoras Apr 12 '17 at 13:39
  • Thanks! Used this to add class to leaflet SVG markers: `L.circle([lat, lng], radius)._path.classList.add("circlemarkerclassname")` – nothingisnecessary Nov 28 '17 at 16:51
76

There is element.classList in the DOM API that works for both HTML and SVG elements. No need for jQuery SVG plugin or even jQuery.

$(".jimmy").click(function() {
    this.classList.add("clicked");
});
Tomas Mikula
  • 6,537
  • 25
  • 39
69

jQuery 3 does not have this problem

One of the changes listed on the jQuery 3.0 revisions is:

add SVG class manipulation (#2199, 20aaed3)

One solution for this issue would be to upgrade to jQuery 3. It works great:

var flip = true;
setInterval(function() {
    // Could use toggleClass, but demonstrating addClass.
    if (flip) {
        $('svg circle').addClass('green');
    }
    else {
        $('svg circle').removeClass('green');
    }
    flip = !flip;
}, 1000);
svg circle {
    fill: red;
    stroke: black;
    stroke-width: 5;
}
svg circle.green {
    fill: green;
}
<script src="https://code.jquery.com/jquery-3.0.0.min.js"></script>

<svg>
    <circle cx="50" cy="50" r="25" />
</svg>

The Problem:

The reason the jQuery class manipulation functions do not work with the SVG elements is because jQuery versions prior to 3.0 use the className property for these functions.

Excerpt from jQuery attributes/classes.js:

cur = elem.nodeType === 1 && ( elem.className ?
    ( " " + elem.className + " " ).replace( rclass, " " ) :
    " "
);

This behaves as expected for HTML elements, but for SVG elements className is a little different. For an SVG element, className is not a string, but an instance of SVGAnimatedString.

Consider the following code:

var test_div = document.getElementById('test-div');
var test_svg = document.getElementById('test-svg');
console.log(test_div.className);
console.log(test_svg.className);
#test-div {
    width: 200px;
    height: 50px;
    background: blue;
}
<div class="test div" id="test-div"></div>

<svg width="200" height="50" viewBox="0 0 200 50">
  <rect width="200" height="50" fill="green" class="test svg" id="test-svg" />
</svg>

If you run this code you will see something like the following in your developer console.

test div
SVGAnimatedString { baseVal="test svg",  animVal="test svg"}

If we were to cast that SVGAnimatedString object to a string as jQuery does, we would have [object SVGAnimatedString], which is where jQuery fails.

How the jQuery SVG plugin handles this:

The jQuery SVG plugin works around this by patching the relevant functions to add SVG support.

Excerpt from jQuery SVG jquery.svgdom.js:

function getClassNames(elem) {
    return (!$.svg.isSVGElem(elem) ? elem.className :
        (elem.className ? elem.className.baseVal : elem.getAttribute('class'))) || '';
}

This function will detect if an element is an SVG element, and if it is it will use the baseVal property of the SVGAnimatedString object if available, before falling back on the class attribute.

jQuery's historical stance on the issue:

jQuery currently lists this issue on their Won’t Fix page. Here is the relevant parts.

SVG/VML or Namespaced Elements Bugs

jQuery is primarily a library for the HTML DOM, so most problems related to SVG/VML documents or namespaced elements are out of scope. We do try to address problems that "bleed through" to HTML documents, such as events that bubble out of SVG.

Evidently jQuery considered full SVG support outside the scope of the jQuery core, and better suited for plugins.

Community
  • 1
  • 1
Alexander O'Mara
  • 58,688
  • 18
  • 163
  • 171
38

If you have dynamic classes or don't know what classes could be already applied then this method I believe is the best approach:

// addClass
$('path').attr('class', function(index, classNames) {
    return classNames + ' class-name';
});

// removeClass
$('path').attr('class', function(index, classNames) {
    return classNames.replace('class-name', '');
});
nav
  • 3,035
  • 1
  • 20
  • 24
  • 1
    This was a livesaver for me as I needed to change a lot of SVG-elements to get things done. +999 from me :) – Karl Jun 18 '15 at 00:05
  • It's hard to believe that jQuery would still have their heels dug in on this, even after 2 years and the release of 2.x.x. Your fix, nav, saved the day for me. The rest of these fixes were unnecessarily complex. Thanks! – unrivaledcreations Aug 16 '15 at 04:49
18

Based on above answers I created the following API

/*
 * .addClassSVG(className)
 * Adds the specified class(es) to each of the set of matched SVG elements.
 */
$.fn.addClassSVG = function(className){
    $(this).attr('class', function(index, existingClassNames) {
        return ((existingClassNames !== undefined) ? (existingClassNames + ' ') : '') + className;
    });
    return this;
};

/*
 * .removeClassSVG(className)
 * Removes the specified class to each of the set of matched SVG elements.
 */
$.fn.removeClassSVG = function(className){
    $(this).attr('class', function(index, existingClassNames) {
        var re = new RegExp('\\b' + className + '\\b', 'g');
        return existingClassNames.replace(re, '');
    });
    return this;
};
Sagar Gala
  • 944
  • 10
  • 10
  • 3
    This is helpful, but the algorithms have issues: 1. If there is no existing class string, you get 'undedfined className' upon adding a class. 2. If the class string contains a class that is the superset of the regular expression, you leave behind junk in the class string. For example, if you try to remove the class 'dog' and the class string contains 'dogfood', you'll be left with 'food' in the class string. Here are some fixes: https://jsfiddle.net/hellobrett/c2c2zfto/ – Brett Dec 15 '15 at 18:31
  • 1
    This is not a perfect solution. It replace all words containing class you want to remove. – tom10271 Dec 23 '15 at 03:27
  • the post has been editing addressing both concerns above. – oromoiluig Jun 16 '22 at 15:28
  • You may want to add ` *` at the beginning of the regexp to avoid leaving whitespaces behind when removing the class – oromoiluig Jun 16 '22 at 15:35
13

After loading jquery.svg.js you must load this file: http://keith-wood.name/js/jquery.svgdom.js.

Source: http://keith-wood.name/svg.html#dom

Working example: http://jsfiddle.net/74RbC/99/

JasCav
  • 34,458
  • 20
  • 113
  • 170
bennedich
  • 12,150
  • 6
  • 33
  • 41
  • Added it - still doesn't seem to work. I've been looking through the documentation on jquery SVG's site, but there is no clear explanation of what you need to do to make use of its extra functionality. – Don P Dec 27 '11 at 00:55
8

Just add the missing prototype constructor to all SVG nodes:

SVGElement.prototype.hasClass = function (className) {
  return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.getAttribute('class'));
};

SVGElement.prototype.addClass = function (className) { 
  if (!this.hasClass(className)) {
    this.setAttribute('class', this.getAttribute('class') + ' ' + className);
  }
};

SVGElement.prototype.removeClass = function (className) {
  var removedClass = this.getAttribute('class').replace(new RegExp('(\\s|^)' + className + '(\\s|$)', 'g'), '$2');
  if (this.hasClass(className)) {
    this.setAttribute('class', removedClass);
  }
};

You can then use it this way without requiring jQuery:

this.addClass('clicked');

this.removeClass('clicked');

All credit goes to Todd Moto.

Ziad
  • 1,036
  • 2
  • 21
  • 31
  • ie11 chokes on this code. this polyfill was a better route: https://github.com/eligrey/classList.js – ryanrain May 30 '19 at 23:15
5

jQuery does not support the classes of SVG elements. You can get the element directly $(el).get(0) and use classList and add / remove. There is a trick with this too in that the topmost SVG element is actually a normal DOM object and can be used like every other element in jQuery. In my project I created this to take care of what I needed but the documentation provided on the Mozilla Developer Network has a shim that can be used as an alternative.

example

function addRemoveClass(jqEl, className, addOrRemove) 
{
  var classAttr = jqEl.attr('class');
  if (!addOrRemove) {
    classAttr = classAttr.replace(new RegExp('\\s?' + className), '');
    jqEl.attr('class', classAttr);
  } else {
    classAttr = classAttr + (classAttr.length === 0 ? '' : ' ') + className;
    jqEl.attr('class', classAttr);
  }
}

An alternative all tougher is to use D3.js as your selector engine instead. My projects have charts that are built with it so it's also in my app scope. D3 correctly modifies the class attributes of vanilla DOM elements and SVG elements. Though adding D3 for just this case would likely be overkill.

d3.select(el).classed('myclass', true);
QueueHammer
  • 10,515
  • 12
  • 67
  • 91
  • 1
    Thanks for that d3 bit... I actually already had d3 on the page so just decided to use that. Very useful :) – Muers Dec 02 '14 at 19:29
5

jQuery 2.2 supports SVG class manipulation

The jQuery 2.2 and 1.12 Released post includes the following quote:

While jQuery is a HTML library, we agreed that class support for SVG elements could be useful. Users will now be able to call the .addClass(), .removeClass(), .toggleClass(), and .hasClass() methods on SVG. jQuery now changes the class attribute rather than the className property. This also makes the class methods usable in general XML documents. Keep in mind that many other things will not work with SVG, and we still recommend using a library dedicated to SVG if you need anything beyond class manipulation.

Example using jQuery 2.2.0

It tests:

  • .addClass()
  • .removeClass()
  • .hasClass()

If you click on that small square, it will change its color because the class attribute is added / removed.

$("#x").click(function() {
    if ( $(this).hasClass("clicked") ) {
        $(this).removeClass("clicked");
    } else {
        $(this).addClass("clicked");
    }
});
.clicked {
    fill: red !important;  
}
<html>

<head>
    <script src="https://code.jquery.com/jquery-2.2.0.js"></script>
</head>

<body>
    <svg width="80" height="80">
        <rect id="x" width="80" height="80" style="fill:rgb(0,0,255)" />
    </svg>
</body>

</html>
ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
4

I use Snap.svg to add a class to and SVG.

var jimmy = Snap(" .jimmy ")

jimmy.addClass("exampleClass");

http://snapsvg.io/docs/#Element.addClass

cjohndesign
  • 178
  • 6
3

Here is my rather inelegant but working code that deals with the following issues (without any dependencies):

  • classList not existing on <svg> elements in IE
  • className not representing the class attribute on <svg> elements in IE
  • Old IE's broken getAttribute() and setAttribute() implementations

It uses classList where possible.

Code:

var classNameContainsClass = function(fullClassName, className) {
    return !!fullClassName &&
           new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName);
};

var hasClass = function(el, className) {
    if (el.nodeType !== 1) {
        return false;
    }
    if (typeof el.classList == "object") {
        return (el.nodeType == 1) && el.classList.contains(className);
    } else {
        var classNameSupported = (typeof el.className == "string");
        var elClass = classNameSupported ? el.className : el.getAttribute("class");
        return classNameContainsClass(elClass, className);
    }
};

var addClass = function(el, className) {
    if (el.nodeType !== 1) {
        return;
    }
    if (typeof el.classList == "object") {
        el.classList.add(className);
    } else {
        var classNameSupported = (typeof el.className == "string");
        var elClass = classNameSupported ?
            el.className : el.getAttribute("class");
        if (elClass) {
            if (!classNameContainsClass(elClass, className)) {
                elClass += " " + className;
            }
        } else {
            elClass = className;
        }
        if (classNameSupported) {
            el.className = elClass;
        } else {
            el.setAttribute("class", elClass);
        }
    }
};

var removeClass = (function() {
    function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
        return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
    }

    return function(el, className) {
        if (el.nodeType !== 1) {
            return;
        }
        if (typeof el.classList == "object") {
            el.classList.remove(className);
        } else {
            var classNameSupported = (typeof el.className == "string");
            var elClass = classNameSupported ?
                el.className : el.getAttribute("class");
            elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer);
            if (classNameSupported) {
                el.className = elClass;
            } else {
                el.setAttribute("class", elClass);
            }
        }
    }; //added semicolon here
})();

Example usage:

var el = document.getElementById("someId");
if (hasClass(el, "someClass")) {
    removeClass(el, "someClass");
}
addClass(el, "someOtherClass");
art guy
  • 35
  • 1
  • 9
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • Did you test it using IE7? Because LTE IE7 _requires strAttributeName to be "className" when setting, getting, or removing a CLASS attribute_. – Knu Jun 08 '15 at 09:25
  • @Knu: It definitely works in IE 6 and 7 for regular HTML elements because in those browsers it uses the `className` property rather than trying to set an attribute. The reason that "LTE IE7 requires strAttributeName to be "className" when setting, getting, or removing a CLASS attribute" is that those browsers have a broken implementation of `getAttribute(x)` and `setAttribute(x)` that simply get or set the property with name `x`, so it's easier and more compatible to set the property directly. – Tim Down Jun 08 '15 at 11:21
  • @Knu: OK. I wasn't sure since SVG is not supported in IE 7, so I'm not sure what you'd hope to achieve by including an `` element in a page that is designed to work in IE 7. For what it's worth, my code does add a class attribute successfully to `` elements in IE 7 since such an element behaves like any other custom element. – Tim Down Jun 08 '15 at 23:11
  • Completely forgot about that thanks for the reminder. Ill try to get my hands on Adobe SVG Viewer to check if it works as intended. – Knu Jun 09 '15 at 08:53
1

One workaround could be to addClass to a container of the svg element:

$('.svg-container').addClass('svg-red');
.svg-red svg circle{
    fill: #ED3F32;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="svg-container">
  <svg height="40" width="40">
      <circle cx="20" cy="20" r="20"/>
  </svg>
</div>
Claytronicon
  • 1,437
  • 13
  • 14
1

I wrote this in my project, and it works... probably;)

$.fn.addSvgClass = function(className) {

    var attr
    this.each(function() {
        attr = $(this).attr('class')
        if(attr.indexOf(className) < 0) {
            $(this).attr('class', attr+' '+className+ ' ')
        }
    })
};    

$.fn.removeSvgClass = function(className) {

    var attr
    this.each(function() {
        attr = $(this).attr('class')
        attr = attr.replace(className , ' ')
        $(this).attr('class' , attr)
    })
};    

examples

$('path').addSvgClass('fillWithOrange')
$('path').removeSvgClass('fillWithOrange')
Dariusz Majchrzak
  • 1,227
  • 2
  • 12
  • 22
0

Inspired by the answers above, especially by Sagar Gala, I've created this API. You may use it if you don't want or can't upgrade your jquery version.

Stonecrusher
  • 184
  • 1
  • 2
  • 14
-1

Or just use old-school DOM methods when JQ has a monkey in the middle somewhere.

var myElement = $('#my_element')[0];
var myElClass = myElement.getAttribute('class').split(/\s+/g);
//splits class into an array based on 1+ white space characters

myElClass.push('new_class');

myElement.setAttribute('class', myElClass.join(' '));

//$(myElement) to return to JQ wrapper-land

Learn the DOM people. Even in 2016's framework-palooza it helps quite regularly. Also, if you ever hear someone compare the DOM to assembly, kick them for me.

Erik Reppen
  • 4,605
  • 1
  • 22
  • 26