This is a self Q&A of a handy piece of code I came up with.
Currently, there isn't an easy way to embed an SVG image and then have access to the SVG elements via CSS. There are various methods of using JS SVG frameworks, but they are overly complicated if all you are doing is making a simple icon with a rollover state.
So here is what I came up with, which I think is by far the easiest way to use SVG files on a website. It takes its concept from the early text-to-image replacement methods, but as far as I am aware has never been done for SVGs.
This is the question:
How do I embed an SVG and change its color in CSS without using a JS-SVG framework?
Firstly, use an IMG tag in your HTML to embed an SVG graphic. I used Adobe Illustrator to make the graphic.
<img id="facebook-logo" class="svg social-link" src="/images/logo-facebook.svg"/>
This is just like how you'd embed a normal image. Note that you need to set the IMG to have a class of svg. The 'social-link' class is just for examples sake. The ID is not required, but is useful.
Then use this jQuery code (in a separate file or inline in the HEAD).
/**
* Replace all SVG images with inline SVG
*/
jQuery('img.svg').each(function(){
var $img = jQuery(this);
var imgID = $img.attr('id');
var imgClass = $img.attr('class');
var imgURL = $img.attr('src');
jQuery.get(imgURL, function(data) {
// Get the SVG tag, ignore the rest
var $svg = jQuery(data).find('svg');
// Add replaced image's ID to the new SVG
if(typeof imgID !== 'undefined') {
$svg = $svg.attr('id', imgID);
}
// Add replaced image's classes to the new SVG
if(typeof imgClass !== 'undefined') {
$svg = $svg.attr('class', imgClass+' replaced-svg');
}
// Remove any invalid XML tags as per http://validator.w3.org
$svg = $svg.removeAttr('xmlns:a');
// Replace image with new SVG
$img.replaceWith($svg);
}, 'xml');
});
What the above code does is look for all IMG's with the class 'svg' and replace it with the inline SVG from the linked file. The massive advantage is that it allows you to use CSS to change the color of the SVG now, like so:
svg:hover path {
fill: red;
}
The jQuery code I wrote also ports across the original images ID and classes. So this CSS works too:
#facebook-logo:hover path {
fill: red;
}
Or:
.social-link:hover path {
fill: red;
}
You can see an example of it working here: http://labs.funkhausdesign.com/examples/img-svg/img-to-svg.html
We have a more complicated version that includes caching here: https://github.com/funkhaus/style-guide/blob/master/template/js/site.js#L32-L90
Style
svg path {
fill: #000;
}
Script
$(document).ready(function() {
$('img[src$=".svg"]').each(function() {
var $img = jQuery(this);
var imgURL = $img.attr('src');
var attributes = $img.prop("attributes");
$.get(imgURL, function(data) {
// Get the SVG tag, ignore the rest
var $svg = jQuery(data).find('svg');
// Remove any invalid XML tags
$svg = $svg.removeAttr('xmlns:a');
// Loop through IMG attributes and apply on SVG
$.each(attributes, function() {
$svg.attr(this.name, this.value);
});
// Replace IMG with SVG
$img.replaceWith($svg);
}, 'xml');
});
});
width="170.667"
in my case
path
shapes (like rect
) you'd need to handle them in css as well
You can now use the CSS filter
property in most modern browsers (including Edge, but not IE11). It works on SVG images as well as other elements. You can use hue-rotate
or invert
to modify colors, although they don't let you modify different colors independently. I use the following CSS class to show a "disabled" version of an icon (where the original is an SVG picture with saturated color):
.disabled {
opacity: 0.4;
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
}
This makes it light grey in most browsers. In IE (and probably Opera Mini, which I haven't tested) it is noticeably faded by the opacity property, which still looks pretty good, although it's not grey.
Here's an example with four different CSS classes for the Twemoji bell icon: original (yellow), the above "disabled" class, hue-rotate
(green), and invert
(blue).
.twa-bell { background-image: url("https://twemoji.maxcdn.com/svg/1f514.svg"); display: inline-block; background-repeat: no-repeat; background-position: center center; height: 3em; width: 3em; margin: 0 0.15em 0 0.3em; vertical-align: -0.3em; background-size: 3em 3em; } .grey-out { opacity: 0.4; filter: grayscale(100%); -webkit-filter: grayscale(100%); } .hue-rotate { filter: hue-rotate(90deg); -webkit-filter: hue-rotate(90deg); } .invert { filter: invert(100%); -webkit-filter: invert(100%); }
if ($('.w3-top img').css("color") == "rgb(0, 0, 0)") { $('.w3-top img').css("filter", "invert(100%)"); $('.w3-top img').css("-webkit-filter", "invert(100%)"); };
Alternatively you could use CSS mask
, granted browser support isn't good but you could use a fallback
.frame {
background: blue;
-webkit-mask: url(image.svg) center / contain no-repeat;
}
-webkit-mask
should not be used on any production website.
If you can include files (PHP include or include via your CMS of choice) in your page, you can add the SVG code and include it into your page. This works the same as pasting the SVG source into the page, but makes the page markup cleaner.
The benefit is that you can target parts of your SVG via CSS for hover -- no javascript required.
http://codepen.io/chriscoyier/pen/evcBu
You just have to use a CSS rule like this:
#pathidorclass:hover { fill: #303 !important; }
Note that the !important
bit is necessary to override the fill color.
<div ng-include="'svg.svg'"></div>
#pathidorclass:hover, .wrapperclass:hover #pathidorclass { fill: green; }
Or even just eliminate the original hover of the SVG path since you are targeting it via the wrapper element now anyway.
TL/DR: GO here-> https://codepen.io/sosuke/pen/Pjoqqp
Explanation:
I'm assuming you have html something like this:
<img src="/img/source.svg" class="myClass">
Definitely go the filter route, ie. your svg is most likely black or white. You can apply a filter to get it to be whatever color you want, for example, I have a black svg that I want mint green. I first invert it to be white (which is technically all RGB colors on full) then play with the hue saturation etc. To get it right:
filter: invert(86%) sepia(21%) saturate(761%) hue-rotate(92deg) brightness(99%) contrast(107%);
Even better is that you could just use a tool to convert the hex you want into a filter for you: https://codepen.io/sosuke/pen/Pjoqqp
@Drew Baker gave a great solution to solve the problem. The code works properly. However, those who uses AngularJs may find lots of dependency on jQuery. Consequently, I thought it is a good idea to paste for AngularJS users, a code following @Drew Baker's solution.
AngularJs way of the same code
1. Html: use the bellow tag in you html file:
<svg-image src="/icons/my.svg" class="any-class-you-wish"></svg-image>
2. Directive: this will be the directive that you will need to recognise the tag:
'use strict';
angular.module('myApp')
.directive('svgImage', ['$http', function($http) {
return {
restrict: 'E',
link: function(scope, element) {
var imgURL = element.attr('src');
// if you want to use ng-include, then
// instead of the above line write the bellow:
// var imgURL = element.attr('ng-include');
var request = $http.get(
imgURL,
{'Content-Type': 'application/xml'}
);
scope.manipulateImgNode = function(data, elem){
var $svg = angular.element(data)[4];
var imgClass = elem.attr('class');
if(typeof(imgClass) !== 'undefined') {
var classes = imgClass.split(' ');
for(var i = 0; i < classes.length; ++i){
$svg.classList.add(classes[i]);
}
}
$svg.removeAttribute('xmlns:a');
return $svg;
};
request.success(function(data){
element.replaceWith(scope.manipulateImgNode(data, element));
});
}
};
}]);
3. CSS:
.any-class-you-wish{
border: 1px solid red;
height: 300px;
width: 120px
}
4. Unit-test with karma-jasmine:
'use strict';
describe('Directive: svgImage', function() {
var $rootScope, $compile, element, scope, $httpBackend, apiUrl, data;
beforeEach(function() {
module('myApp');
inject(function($injector) {
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
$httpBackend = $injector.get('$httpBackend');
apiUrl = $injector.get('apiUrl');
});
scope = $rootScope.$new();
element = angular.element('<svg-image src="/icons/icon-man.svg" class="svg"></svg-image>');
element = $compile(element)(scope);
spyOn(scope, 'manipulateImgNode').andCallThrough();
$httpBackend.whenGET(apiUrl + 'me').respond(200, {});
data = '<?xml version="1.0" encoding="utf-8"?>' +
'<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->' +
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' +
'<!-- Obj -->' +
'<!-- Obj -->' +
'<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"' +
'width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">' +
'<g>' +
'<path fill="#F4A902" d=""/>' +
'<path fill="#F4A902" d=""/>' +
'</g>' +
'</svg>';
$httpBackend.expectGET('/icons/icon-man.svg').respond(200, data);
});
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should call manipulateImgNode atleast once', function () {
$httpBackend.flush();
expect(scope.manipulateImgNode.callCount).toBe(1);
});
it('should return correct result', function () {
$httpBackend.flush();
var result = scope.manipulateImgNode(data, element);
expect(result).toBeDefined();
});
it('should define classes', function () {
$httpBackend.flush();
var result = scope.manipulateImgNode(data, element);
var classList = ["svg"];
expect(result.classList[0]).toBe(classList[0]);
});
});
<div ng-include="/icons/my.svg" class="any-class-you-wish"></div>
ng-include
then just change this line var imgURL = element.attr('src');
to var imgURL = element.attr('ng-include');
if (typeof(imgClass) !== 'undefined') { $svg.setAttribute("class", imgClass); }
instead of split and for loop.
angular.element(data)[0];
) and to make it work with IE use if ($svg.getAttribute('class')) { $svg.setAttribute('class', $svg.getAttribute('class') + ' ' + imgClass); } else { $svg.setAttribute('class', imgClass); }
. Also you might want to add cache: true
to the options of $http.get
otherwise your page might become very slow.
I realize you're wanting to accomplish this with CSS, but just a reminder in case it's a small, simple image - you can always pop it open in Notepad++ and change the path/whateverelement's fill:
<path style="fill:#010002;" d="M394.854,205.444c9.218-15.461,19.102-30.181,14.258-49.527
...
C412.843,226.163,402.511,211.451,394.854,205.444z"/>
It could save a ton of ugly script. Sorry if it's off-base, but sometimes the simple solutions can be overlooked.
...even swapping multiple svg images might be smaller in size than some of the code snippets for this question.
I wrote a directive to solve this issue with AngularJS. It is available here - ngReusableSvg.
It replaces the SVG element after it's been rendered, and places it inside a div
element, making its CSS easily changeable. This helps using the same SVG file in different places using different sizes/colors.
The usage is simple:
<object oa-reusable-svg
data="my_icon.svg"
type="image/svg+xml"
class="svg-class"
height="30" // given to prevent UI glitches at switch time
width="30">
</object>
After that, you can easily have:
.svg-class svg {
fill: red; // whichever color you want
}
Here's a version for knockout.js
based on the accepted answer:
Important: It does actually require jQuery too for the replacing, but I thought it may be useful to some.
ko.bindingHandlers.svgConvert =
{
'init': function ()
{
return { 'controlsDescendantBindings': true };
},
'update': function (element, valueAccessor, allBindings, viewModel, bindingContext)
{
var $img = $(element);
var imgID = $img.attr('id');
var imgClass = $img.attr('class');
var imgURL = $img.attr('src');
$.get(imgURL, function (data)
{
// Get the SVG tag, ignore the rest
var $svg = $(data).find('svg');
// Add replaced image's ID to the new SVG
if (typeof imgID !== 'undefined')
{
$svg = $svg.attr('id', imgID);
}
// Add replaced image's classes to the new SVG
if (typeof imgClass !== 'undefined')
{
$svg = $svg.attr('class', imgClass + ' replaced-svg');
}
// Remove any invalid XML tags as per http://validator.w3.org
$svg = $svg.removeAttr('xmlns:a');
// Replace image with new SVG
$img.replaceWith($svg);
}, 'xml');
}
};
Then just apply data-bind="svgConvert: true"
to your img tag.
This solution completely replaces the img
tag with a SVG and any additional bindings would not be respected.
get
would request it again. I considered changing the src
to a data-src
attribute on the img
tag, but concluded that modern browsers are probably smart enough to cache the file anyway
There is an open source library called SVGInject that uses the onload
attribute to trigger the injection. You can find the GitHub project at https://github.com/iconfu/svg-inject
Here is a minimal example using SVGInject:
<html>
<head>
<script src="svg-inject.min.js"></script>
</head>
<body>
<img src="image.svg" onload="SVGInject(this)" />
</body>
</html>
After the image is loaded the onload="SVGInject(this)
will trigger the injection and the <img>
element will be replaced by the contents of the SVG file provided in the src
attribute.
It solves several issues with SVG injection:
SVGs can be hidden until injection has finished. This is important if a style is already applied during load time, which would otherwise cause a brief "unstyled content flash". The elements inject themselved automatically. If you add SVGs dynamically, you don't have to worry about calling the injection function again. A random string is added to each ID in the SVG to avoid having the same ID multiple times in the document if an SVG is injected more than once.
SVGInject is plain Javascript and works with all browsers that support SVG.
Disclaimer: I am the co-author of SVGInject
Here's a no framework code, only pure js :
document.querySelectorAll('img.svg').forEach(function(element) {
var imgID = element.getAttribute('id')
var imgClass = element.getAttribute('class')
var imgURL = element.getAttribute('src')
xhr = new XMLHttpRequest()
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && xhr.status == 200) {
var svg = xhr.responseXML.getElementsByTagName('svg')[0];
if(imgID != null) {
svg.setAttribute('id', imgID);
}
if(imgClass != null) {
svg.setAttribute('class', imgClass + ' replaced-svg');
}
svg.removeAttribute('xmlns:a')
if(!svg.hasAttribute('viewBox') && svg.hasAttribute('height') && svg.hasAttribute('width')) {
svg.setAttribute('viewBox', '0 0 ' + svg.getAttribute('height') + ' ' + svg.getAttribute('width'))
}
element.parentElement.replaceChild(svg, element)
}
}
xhr.open('GET', imgURL, true)
xhr.send(null)
})
If we have a greater number of such svg images we can also take the help of font-files.
Sites like https://glyphter.com/ can get us a font file from our svgs.
E.g.
@font-face {
font-family: 'iconFont';
src: url('iconFont.eot');
}
#target{
color: white;
font-size:96px;
font-family:iconFont;
}
You can use data-image for that. using data-image(data-URI) you can access SVG like inline.
Here is rollover effect using pure CSS and SVG.
I know it messy but you can do this way.
.action-btn { background-size: 20px 20px; background-position: center center; background-repeat: no-repeat; border-width: 1px; border-style: solid; border-radius: 30px; height: 40px; width: 60px; display: inline-block; } .delete { background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' id='Capa_1' fill='#FB404B' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='482.428px' height='482.429px' viewBox='0 0 482.428 482.429' style='enable-background:new 0 0 482.428 482.429;' xml:space='preserve'%3e%3cg%3e%3cg%3e%3cpath d='M381.163,57.799h-75.094C302.323,25.316,274.686,0,241.214,0c-33.471,0-61.104,25.315-64.85,57.799h-75.098 c-30.39,0-55.111,24.728-55.111,55.117v2.828c0,23.223,14.46,43.1,34.83,51.199v260.369c0,30.39,24.724,55.117,55.112,55.117 h210.236c30.389,0,55.111-24.729,55.111-55.117V166.944c20.369-8.1,34.83-27.977,34.83-51.199v-2.828 C436.274,82.527,411.551,57.799,381.163,57.799z M241.214,26.139c19.037,0,34.927,13.645,38.443,31.66h-76.879 C206.293,39.783,222.184,26.139,241.214,26.139z M375.305,427.312c0,15.978-13,28.979-28.973,28.979H136.096 c-15.973,0-28.973-13.002-28.973-28.979V170.861h268.182V427.312z M410.135,115.744c0,15.978-13,28.979-28.973,28.979H101.266 c-15.973,0-28.973-13.001-28.973-28.979v-2.828c0-15.978,13-28.979,28.973-28.979h279.897c15.973,0,28.973,13.001,28.973,28.979 V115.744z'/%3e%3cpath d='M171.144,422.863c7.218,0,13.069-5.853,13.069-13.068V262.641c0-7.216-5.852-13.07-13.069-13.07 c-7.217,0-13.069,5.854-13.069,13.07v147.154C158.074,417.012,163.926,422.863,171.144,422.863z'/%3e%3cpath d='M241.214,422.863c7.218,0,13.07-5.853,13.07-13.068V262.641c0-7.216-5.854-13.07-13.07-13.07 c-7.217,0-13.069,5.854-13.069,13.07v147.154C228.145,417.012,233.996,422.863,241.214,422.863z'/%3e%3cpath d='M311.284,422.863c7.217,0,13.068-5.853,13.068-13.068V262.641c0-7.216-5.852-13.07-13.068-13.07 c-7.219,0-13.07,5.854-13.07,13.07v147.154C298.213,417.012,304.067,422.863,311.284,422.863z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e "); border-color:#FB404B; } .delete:hover { background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg version='1.1' id='Capa_1' fill='#fff' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='482.428px' height='482.429px' viewBox='0 0 482.428 482.429' style='enable-background:new 0 0 482.428 482.429;' xml:space='preserve'%3e%3cg%3e%3cg%3e%3cpath d='M381.163,57.799h-75.094C302.323,25.316,274.686,0,241.214,0c-33.471,0-61.104,25.315-64.85,57.799h-75.098 c-30.39,0-55.111,24.728-55.111,55.117v2.828c0,23.223,14.46,43.1,34.83,51.199v260.369c0,30.39,24.724,55.117,55.112,55.117 h210.236c30.389,0,55.111-24.729,55.111-55.117V166.944c20.369-8.1,34.83-27.977,34.83-51.199v-2.828 C436.274,82.527,411.551,57.799,381.163,57.799z M241.214,26.139c19.037,0,34.927,13.645,38.443,31.66h-76.879 C206.293,39.783,222.184,26.139,241.214,26.139z M375.305,427.312c0,15.978-13,28.979-28.973,28.979H136.096 c-15.973,0-28.973-13.002-28.973-28.979V170.861h268.182V427.312z M410.135,115.744c0,15.978-13,28.979-28.973,28.979H101.266 c-15.973,0-28.973-13.001-28.973-28.979v-2.828c0-15.978,13-28.979,28.973-28.979h279.897c15.973,0,28.973,13.001,28.973,28.979 V115.744z'/%3e%3cpath d='M171.144,422.863c7.218,0,13.069-5.853,13.069-13.068V262.641c0-7.216-5.852-13.07-13.069-13.07 c-7.217,0-13.069,5.854-13.069,13.07v147.154C158.074,417.012,163.926,422.863,171.144,422.863z'/%3e%3cpath d='M241.214,422.863c7.218,0,13.07-5.853,13.07-13.068V262.641c0-7.216-5.854-13.07-13.07-13.07 c-7.217,0-13.069,5.854-13.069,13.07v147.154C228.145,417.012,233.996,422.863,241.214,422.863z'/%3e%3cpath d='M311.284,422.863c7.217,0,13.068-5.853,13.068-13.068V262.641c0-7.216-5.852-13.07-13.068-13.07 c-7.219,0-13.07,5.854-13.07,13.07v147.154C298.213,417.012,304.067,422.863,311.284,422.863z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e "); background-color: #FB404B; }
You can convert your svg to data url here
https://codepen.io/elliz/full/ygvgay https://websemantics.uk/tools/svg-to-background-image-conversion/
Since SVG is basically code, you need just contents. I used PHP to obtain content, but you can use whatever you want.
<?php
$content = file_get_contents($pathToSVG);
?>
Then, I've printed content "as is" inside a div container
<div class="fill-class"><?php echo $content;?></div>
To finnaly set rule to container's SVG childs on CSS
.fill-class > svg {
fill: orange;
}
I got this results with a material icon SVG:
Mozilla Firefox 59.0.2 (64-bit) Linux
https://i.stack.imgur.com/hny3K.jpg
Google Chrome66.0.3359.181 (Build oficial) (64 bits) Linux
https://i.stack.imgur.com/shBNk.jpg
Opera 53.0.2907.37 Linux
https://i.stack.imgur.com/bcnwH.jpg
The selected solution is fine if you want jQuery to process all svg elements in your DOM and your DOM is of reasonable size. But if your DOM is large and you decide to load parts of your DOM dynamically, it really makes no sense to have to rescan the entire DOM just to update svg elements. Instead, use a jQuery plugin to do this:
/**
* A jQuery plugin that loads an svg file and replaces the jQuery object with its contents.
*
* The path to the svg file is specified in the src attribute (which normally does not exist for an svg element).
*
* The width, height and class attributes in the loaded svg will be replaced by those that exist in the jQuery object's
* underlying html. Note: All other attributes in the original element are lost including the style attribute. Place
* any styles in a style class instead.
*/
(function ($) {
$.fn.svgLoader = function () {
var src = $(this).attr("src");
var width = this.attr("width");
var height = this.attr("height");
var cls = this.attr("class");
var ctx = $(this);
// Get the svg file and replace the <svg> element.
$.ajax({
url: src,
cache: false
}).done(function (html) {
let svg = $(html);
svg.attr("width", width);
svg.attr("height", height);
svg.attr("class", cls);
var newHtml = $('<a></a>').append(svg.clone()).html();
ctx.replaceWith(newHtml);
});
return this;
};
}(jQuery));
In your html, specify an svg element as follows:
<svg src="images/someSvgFile.svg" height="45" width="45" class="mySVGClass"/>
And apply the plugin:
$(".mySVGClass").svgLoader();
for :hover event animations we can left the styles inside svg file, like a
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
rect {
fill:rgb(165,225,75);
stroke:none;
transition: 550ms ease-in-out;
transform-origin:125px 125px;
}
rect:hover {
fill:rgb(75,165,225);
transform:rotate(360deg);
}
</style>
</defs>
<rect x='50' y='50' width='150' height='150'/>
</svg>
.carousel-control-prev-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='rgb(3,122,247)' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e");
}
chnage color : fill='rgb(3,122,247)'
Success story sharing
});
of $.get with}, 'xml');
img[src$=".svg"]
and eliminate the need for thesvg
class.