diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index f5eaff9066..d8588170b7 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -18,6 +18,7 @@ var MatrixClientPeg = require("./MatrixClientPeg");
 var MatrixTools = require("./MatrixTools");
 var dis = require("./dispatcher");
 var encryption = require("./encryption");
+var Tinter = require("./Tinter");
 
 var reject = function(msg) {
     return {
@@ -42,6 +43,10 @@ var commands = {
         return reject("Usage: /nick <display_name>");
     },
 
+    tint: function(room_id, args) {
+        Tinter.tint(args);
+    },
+
     encrypt: function(room_id, args) {
         if (args == "on") {
             var client = MatrixClientPeg.get();
diff --git a/src/Tinter.js b/src/Tinter.js
new file mode 100644
index 0000000000..be81abd9c2
--- /dev/null
+++ b/src/Tinter.js
@@ -0,0 +1,183 @@
+/*
+Copyright 2015 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+var keyRgb = [
+    "rgb(118, 207, 166)",
+    "rgb(234, 245, 240)",
+    "rgba(118, 207, 166, 0.2)",
+];
+
+// Some algebra workings for calculating the tint % of Vector Green & Light Green
+// x * 118 + (1 - x) * 255 = 234
+// x * 118 + 255 - 255 * x = 234
+// x * 118 - x * 255 = 234 - 255
+// (255 - 118) x = 255 - 234
+// x = (255 - 234) / (255 - 118) = 0.16 
+
+var keyHex = [
+    "#76CFA6",
+    "#EAF5F0",
+];
+
+var cssFixups = [
+    // {
+    //     style: a style object that should be fixed up taken from a stylesheet
+    //     attr: name of the attribute to be clobbered, e.g. 'color'
+    //     index: ordinal of primary, secondary or tertiary
+    // }
+];
+
+// CSS attributes to be fixed up
+var cssAttrs = [
+    "color",
+    "backgroundColor",
+    "borderColor",
+];
+
+var svgFixups = [
+    // {
+    //     node: a SVG node that needs to be fixed up
+    //     attr: name of the attribute to be clobbered, e.g. 'fill'
+    //     index: ordinal of primary, secondary
+    // }
+];
+
+var svgAttrs = [
+    "fill",
+    "stroke",
+];
+
+var cached = false;
+
+function calcCssFixups() {
+    for (var i = 0; i < document.styleSheets.length; i++) {
+        var ss = document.styleSheets[i];
+        for (var j = 0; j < ss.cssRules.length; j++) {
+            var rule = ss.cssRules[j];
+            for (var k = 0; k < cssAttrs.length; k++) {
+                var attr = cssAttrs[k];
+                for (var l = 0; l < keyRgb.length; l++) {
+                    if (rule.style && rule.style[attr] === keyRgb[l]) {
+                        cssFixups.push({
+                            style: rule.style,
+                            attr: attr,
+                            index: l,
+                        });
+                    }
+                }
+            }
+        }
+    }
+}
+
+function calcSvgFixups() {
+    var svgs = document.getElementsByClassName("mx_Svg");
+    for (var i = 0; i < svgs.length; i++) {
+        var svgDoc = svgs[i].contentDocument;
+        if (!svgDoc) continue;
+        var tags = svgDoc.getElementsByTagName("*");
+        for (var j = 0; j < tags.length; j++) {
+            var tag = tags[j];
+            for (var k = 0; k < svgAttrs.length; k++) {
+                var attr = svgAttrs[k];
+                for (var l = 0; l < keyHex.length; l++) {
+                    if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) {
+                        svgFixups.push({
+                            node: tag,
+                            attr: attr,
+                            index: l,
+                        });
+                    }
+                }
+            }
+        }
+    }
+}
+
+function applyCssFixups(primaryColor, secondaryColor, tertiaryColor) {
+    var colors = [primaryColor, secondaryColor, tertiaryColor];
+
+    for (var i = 0; i < cssFixups.length; i++) {
+        var cssFixup = cssFixups[i];
+        cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
+    }
+}
+
+function applySvgFixups(primaryColor, secondaryColor, tertiaryColor) {
+    var colors = [primaryColor, secondaryColor, tertiaryColor];
+
+    for (var i = 0; i < svgFixups.length; i++) {
+        var svgFixup = svgFixups[i];
+        svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
+    }
+}
+
+function hexToRgb(color) {
+    if (color[0] === '#') color = color.slice(1);
+    if (color.length === 3) {
+        color = color[0] + color[0] +
+                color[1] + color[1] +
+                color[2] + color[2];
+    }
+    var val = parseInt(color, 16);
+    var r = (val >> 16) & 255;
+    var g = (val >> 8) & 255;
+    var b = val & 255;
+    return [r, g, b];
+}
+
+function rgbToHex(rgb) {
+    var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
+    return '#' + (0x1000000 + val).toString(16).slice(1)
+}
+
+module.exports = {
+    tint: function(primaryColor, secondaryColor, tertiaryColor) {
+        if (!cached) {
+            calcCssFixups();
+            calcSvgFixups();
+            cached = true;
+        }
+
+        if (!secondaryColor) {
+            var x = 0.16; // average weighting factor calculated from vector green & light green
+            var rgb = hexToRgb(primaryColor);
+            rgb[0] = x * rgb[0] + (1 - x) * 255;
+            rgb[1] = x * rgb[1] + (1 - x) * 255;
+            rgb[2] = x * rgb[2] + (1 - x) * 255;
+            secondaryColor = rgbToHex(rgb);
+        }
+
+        if (!tertiaryColor) {
+            var x = 0.19;
+            var rgb1 = hexToRgb(primaryColor);
+            var rgb2 = hexToRgb(secondaryColor);
+            rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
+            rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
+            rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
+            tertiaryColor = rgbToHex(rgb1);
+        }
+
+        // go through manually fixing up the stylesheets.
+        applyCssFixups(primaryColor, secondaryColor, tertiaryColor);
+
+        // go through manually fixing up SVG colours.
+        // we could do this by stylesheets, but keeping the stylesheets
+        // updated would be a PITA, so just brute-force search for the
+        // key colour; cache the element and apply.
+        applySvgFixups(primaryColor, secondaryColor, tertiaryColor);
+    }
+};
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index ea055e594b..5dec8c6139 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -1194,7 +1194,7 @@ module.exports = React.createClass({
                             <div className="mx_RoomView_tabCompleteWrapper">
                                 <TabCompleteBar entries={this.tabComplete.peek(6)} />
                                 <div className="mx_RoomView_tabCompleteEol" title="->|">
-                                    <object type="image/svg+xml" data="img/eol.svg" width="22" height="16"/>
+                                    <object className="mx_Svg" type="image/svg+xml" data="img/eol.svg" width="22" height="16"/>
                                     Auto-complete
                                 </div>
                             </div>
@@ -1268,7 +1268,7 @@ module.exports = React.createClass({
             if (this.state.draggingFile) {
                 fileDropTarget = <div className="mx_RoomView_fileDropTarget">
                                     <div className="mx_RoomView_fileDropTargetLabel" title="Drop File Here">
-                                        <object type="image/svg+xml" data="img/upload-big.svg" width="45" height="59"/><br/>
+                                        <object className="mx_Svg" type="image/svg+xml" data="img/upload-big.svg" width="45" height="59"/><br/>
                                         Drop File Here
                                     </div>
                                  </div>;
@@ -1306,7 +1306,7 @@ module.exports = React.createClass({
                 if (call.type === "video") {
                     zoomButton = (
                         <div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title="Fill screen">
-                            <object type="image/svg+xml" data="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/>
+                            <object className="mx_Svg" type="image/svg+xml" data="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/>
                         </div>
                     );
 
@@ -1338,7 +1338,7 @@ module.exports = React.createClass({
                         { videoMuteButton }
                         { zoomButton }
                         { statusBar }
-                        <object type="image/svg+xml" className="mx_RoomView_voipChevron" data="img/voip-chevron.svg" width="22" height="17"/>
+                        <object className="mx_Svg" type="image/svg+xml" className="mx_RoomView_voipChevron" data="img/voip-chevron.svg" width="22" height="17"/>
                     </div>
             }
 
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index d5863cdfef..512b2eb109 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -474,11 +474,11 @@ module.exports = React.createClass({
         else {
             callButton =
                 <div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
-                    <object type="image/svg+xml" data="img/voice.svg" width="16" height="26"/>
+                    <object className="mx_Svg" type="image/svg+xml" data="img/voice.svg" width="16" height="26"/>
                 </div>
             videoCallButton =
                 <div className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
-                    <object type="image/svg+xml" data="img/call.svg" width="30" height="22"/>
+                    <object className="mx_Svg" type="image/svg+xml" data="img/call.svg" width="30" height="22"/>
                 </div>
         }
 
@@ -493,7 +493,7 @@ module.exports = React.createClass({
                         <textarea ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
                     </div>
                     <div className="mx_MessageComposer_upload" onClick={this.onUploadClick} title="Upload file">
-                        <object type="image/svg+xml" data="img/upload.svg" width="19" height="24"/>
+                        <object className="mx_Svg" type="image/svg+xml" data="img/upload.svg" width="19" height="24"/>
                         <input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} />
                     </div>
                     { hangupButton }
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 7f13e8c655..6f1c884f43 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -119,7 +119,7 @@ module.exports = React.createClass({
                         <div className="mx_RoomHeader_nametext" title={ this.props.room.name }>{ this.props.room.name }</div>
                         { searchStatus }
                         <div className="mx_RoomHeader_settingsButton" title="Settings">
-                            <object type="image/svg+xml" data="img/settings.svg" width="12" height="12"/>
+                            <object className="mx_Svg" type="image/svg+xml" data="img/settings.svg" width="12" height="12"/>
                         </div>
                     </div>
                 if (topic) topic_el = <div className="mx_RoomHeader_topic" title={topic.getContent().topic}>{ topic.getContent().topic }</div>;
@@ -136,7 +136,7 @@ module.exports = React.createClass({
             if (this.props.onLeaveClick) {
                 leave_button =
                     <div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onLeaveClick} title="Leave room">
-                        <object type="image/svg+xml" data="img/leave.svg"
+                        <object className="mx_Svg" type="image/svg+xml" data="img/leave.svg"
                             width="26" height="20"/>
                     </div>;
             }
@@ -145,7 +145,7 @@ module.exports = React.createClass({
             if (this.props.onForgetClick) {
                 forget_button =
                     <div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onForgetClick} title="Forget room">
-                        <object type="image/svg+xml" data="img/leave.svg"
+                        <object className="mx_Svg" type="image/svg+xml" data="img/leave.svg"
                             width="26" height="20"/>
                     </div>;
             }
@@ -167,7 +167,7 @@ module.exports = React.createClass({
                         { forget_button }
                         { leave_button }
                         <div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
-                            <object type="image/svg+xml" data="img/search.svg" width="21" height="19"/>
+                            <object className="mx_Svg" type="image/svg+xml" data="img/search.svg" width="21" height="19"/>
                         </div>
                     </div>
                 </div>