11

How in JavaScript do you retrieve the styles that have been applied to an element, excluding the default user agent styles (so inline + stylesheet styles only).

Basically, all the user styles you can see in the Computed tab of your favorite developer tool:

User styles displayed in Edge's developer tool

No framework please, IE8+, Edge, Chrome and Firefox.

I am expecting the answer to be the result of getComputedStyle minus getDefaultComputedStyle, but in a cross browser way. Seeing that all developer tools are capable of doing it, there must be a solution :)

Gyum Fox
  • 3,287
  • 2
  • 41
  • 71
  • window.getComputedStyle() ? – Alex K. Feb 03 '17 at 13:23
  • Possible duplicate of [How to get all the applied styles of an element by just giving its id?](http://stackoverflow.com/questions/9430659/how-to-get-all-the-applied-styles-of-an-element-by-just-giving-its-id) – n_plum Feb 03 '17 at 13:24
  • @AlexK.: nope I don't want the default user agent styles: https://jsfiddle.net/701tg35d/ – Gyum Fox Feb 03 '17 at 13:33
  • @n_palum neither that question or its answer excludes the default user agent-style for the result – Gyum Fox Feb 03 '17 at 13:41
  • `getDefaultComputedStyle` is non-standard, so not an option. Developer tools are probably hardcoded to know what the browser applies, so this might not be possible via javascript. – Rik Lewis Feb 03 '17 at 13:56

5 Answers5

10

There is a read only property of document called 'styleSheets'.

var styleSheetList = document.styleSheets;

https://developer.mozilla.org/en-US/docs/Web/API/Document/styleSheets

By using this, you can reach all the styles which are applied by the author.

There is a similar question about this but not a duplicate, in here:

Is it possible to check if certain CSS properties are defined inside the style tag with Javascript?

You can get the applied style from an element, excluding the default user agent styles using the accepted answer of that question i just mentioned.

That answer didn't supply the element's own style attribute content, so i have improved the code a bit:

var proto = Element.prototype;
var slice = Function.call.bind(Array.prototype.slice);
var matches = Function.call.bind(proto.matchesSelector || 
                proto.mozMatchesSelector || proto.webkitMatchesSelector ||
                proto.msMatchesSelector || proto.oMatchesSelector);

// Returns true if a DOM Element matches a cssRule
var elementMatchCSSRule = function(element, cssRule) {
  return matches(element, cssRule.selectorText);
};

// Returns true if a property is defined in a cssRule
var propertyInCSSRule = function(prop, cssRule) {
  return prop in cssRule.style && cssRule.style[prop] !== "";
};

// Here we get the cssRules across all the stylesheets in one array
var cssRules = slice(document.styleSheets).reduce(function(rules, styleSheet) {
  return rules.concat(slice(styleSheet.cssRules));
}, []);




var getAppliedCss = function(elm) {
 // get only the css rules that matches that element
 var elementRules = cssRules.filter(elementMatchCSSRule.bind(null, elm));
 var rules =[];
 if(elementRules.length) {
  for(i = 0; i < elementRules.length; i++) {
   var e = elementRules[i];
   rules.push({
    order:i,
    text:e.cssText
   })
  }  
 }
 
 if(elm.getAttribute('style')) {
  rules.push({
    order:elementRules.length,
    text:elm.getAttribute('style')
   })
 }
 return rules;
}







function showStyle(){
var styleSheetList = document.styleSheets;
// get a reference to an element, then...
var div1 = document.getElementById("div1");

var rules = getAppliedCss(div1);

var str = '';
for(i = 0; i < rules.length; i++) {
   var r = rules[i];
   str += '<br/>Style Order: ' + r.order + ' | Style Text: ' + r.text; 
  }  
  
 document.getElementById("p1").innerHTML = str; 

}
#div1 {
float:left;
width:100px;
}

div {
text-align:center;
}
<div id="div1" style="font-size:14px;">
 Lorem ipsum 
 </div>
<br/>
<br/>
<a href="javascript:;" onclick="showStyle()"> Show me the style. </a>
 <p id="p1"><p>
Community
  • 1
  • 1
er-han
  • 1,859
  • 12
  • 20
  • 1
    I do like this approach, but I want to remark that it currently ignores inline styles. One should add the [`element.style`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) to the results to make it complete. – Just a student Feb 06 '17 at 13:42
  • Right, i see it now. Let me think what i can do – er-han Feb 06 '17 at 13:44
  • 1
    I added element's own style attribute content into the code sample – er-han Feb 06 '17 at 14:27
  • This seems like a very manual approach to the problem, but I reckon that it may be the only cross-browser one as it simply emulates how browsers apply styling rules. Would you consider enhancing your answer with a fiddle so I can give it a try on several browsers and see if I can fault it? Thanks! – Gyum Fox Feb 06 '17 at 18:19
  • @er-han nevermind my request for a Fiddle, I was using the mobile version of Stackoverflow and I didn't get the "Run code snippet" button. – Gyum Fox Feb 07 '17 at 08:05
  • 1
    @GyumFox I wonder if you tried this approach on several browsers? If so, can you share the results? – er-han Feb 07 '17 at 12:46
  • @er-han OK I was able to successfully test your sample on IE9+, Chrome, Firefox and Edge and it works as expected. Getting it to work on IE8 might be a bit harder as it does not support the matchesSelector API. Nevermind, I'll award the bounty as the solution seems robust enough to me. – Gyum Fox Feb 10 '17 at 12:50
  • @GyumFox I am glad it worked, and thank you for the bounty. I am planning to make an extension and clean method to extract the custom-applied CSS. This code on the answer doesn't look good to me :) – er-han Feb 10 '17 at 14:27
  • 1
    I've used the code of your answer here: https://stackoverflow.com/questions/50567761/how-to-display-all-css-selectors-properties-in-a-textarea/50569764# Maybe you'll want to see what I modified! – Takit Isy May 28 '18 at 18:58
5

All developer tools can cheat, because they have access to the default rules the browser they are built into applies.

I thought that the following approach might work.

  1. Construct an element of exactly the same type (say, a div or p) as the one we are interested in.
  2. Append this element somewhere on the page so that only default browser rules are applied. We can do so by putting it in an iframe.
    If you are sure that you do not have rules targeting any p element, for example, then appending to the body may be more efficient.
  3. Check the difference in styles and only report values that differ.
  4. Clean up temporary element(s).

It seems to work reasonably well in practice. I have only tested this in Firefox and Chrome, but I think that it should work in other browsers too - except maybe for the fact that I used for...in and for...of, but one could easily rewrite that. Note that not just the properties you specify are reported, but also some properties that are influenced by the properties you do specify. For example, the border color matches the text color by design and is hence reported as different even when you only set color: white.

To summarize, I have taken the example you posted in one of your comments and added a getNonDefaultStyles function to it that I think does what you want. It can of course be modified to cache default styles of say, div elements and thus be more efficient in repeated calls (because modifying the DOM is expensive), but it shows the gist.

The below snippet shows how the version can be implemented that appends an element to the body. Due to limitations on StackOverflow, it is not possible to show the iframe version in a snippet. It is possible on JSFiddle. The below snippet can also be found in a Fiddle.

var textarea = document.getElementById("textarea"),
    paragraph = document.getElementById("paragraph");

/**
 * Computes applied styles, assuming no rules targeting a specific element.
 */
function getNonDefaultStyles(el) {
  var styles = {},
    computed = window.getComputedStyle(el),
    notTargetedContainer = document.createElement('div'),
    elVanilla = document.createElement(el.tagName);
  document.body.appendChild(notTargetedContainer);
  notTargetedContainer.appendChild(elVanilla);
  var vanilla = window.getComputedStyle(elVanilla);
  for (let key of computed) {
    if (vanilla[key] !== computed[key]) {
      styles[key] = computed[key];
    }
  }
  document.body.removeChild(notTargetedContainer);
  return styles;
}

var paragraphStyles = getNonDefaultStyles(paragraph);
for (let style in paragraphStyles) {
  textarea.value += style + ": " + paragraphStyles[style] + "\n";
}
#paragraph {
  background: red;
}

textarea {
  width: 300px;
  height: 400px;
}
<p id="paragraph" style="color: white">
  I am a DIV
</p>

<p>
  User styles:
</p>
<textarea id="textarea"></textarea>
Community
  • 1
  • 1
Just a student
  • 10,560
  • 2
  • 41
  • 69
  • I do like the idea, but point 2 might be a show stopper as you will not be able to eliminate the styles applied directly to the all elements of a given type (eg: P {margin:0}). I'd like to tweak the solution to handle those. Maybe by reading the style from those elements directly from the stylesheets (inspired by er-han's solution) or by loading a blank page in an iframe to fetch the default styles for a given element type? – Gyum Fox Feb 06 '17 at 18:11
  • 1
    Thanks for the update. I've slightly changed the Fiddle to have it working on Edge: https://jsfiddle.net/9o178w97/2/ (I used for ... in, which has some drawbacks as you said). Although your solution is the closest to my original idea, I've accepted @er-han's answer as it has the advantage of only returning the style properties that have been explicitly set by the user. – Gyum Fox Feb 10 '17 at 13:03
  • 1
    This is a great solution! I've added a couple of improvements (see my answer below) to hide the ` – Joseph238 May 11 '22 at 16:03
3

Here's a function that gets all the CSS rules that have been applied to an element from either inline styles (HTML style attribute) or stylesheets on the page. It also grabs relevant keyframes for CSS animations and the :active, :hover, ::before, and ::after selectors.

function getAppliedCssData(el) {
  // we create a unique id so we can generate unique ids for renaming animations
  let uniqueId = "id" + Math.random().toString().slice(2) + Math.random().toString().slice(2);

  let allRules = [...document.styleSheets].map(s => {
    let rules = [];
    try { rules.push(...s.cssRules) } catch(e) {} // we ignore cross-domain stylesheets with restrictive CORs headers
    return rules;
  }).flat();

  let styleRules = allRules.filter(rule => rule.type === CSSRule.STYLE_RULE)
  let fontFaceRules = allRules.filter(rule => rule.type === CSSRule.FONT_FACE_RULE);
  let keyframesRules = allRules.filter(rule => rule.type === CSSRule.KEYFRAMES_RULE);

  let matchingDefaultRules = styleRules.filter(rule => el.matches(rule.selectorText));
  let nonMatchingRules = styleRules.filter(rule => !el.matches(rule.selectorText));
  let matchingHoverRules =  nonMatchingRules.filter(rule => el.matches(rule.selectorText.replace(/ :/g, " *:").replace(/([^(])(:hover)\b/g, "$1")));
  let matchingActiveRules = nonMatchingRules.filter(rule => el.matches(rule.selectorText.replace(/ :/g, " *:").replace(/([^(])(:active)\b/g, "$1")));
  let matchingBeforeRules = nonMatchingRules.filter(rule => el.matches(rule.selectorText.replace(/ :/g, " *:").replace(/::before\b/g, "")));
  let matchingAfterRules =  nonMatchingRules.filter(rule => el.matches(rule.selectorText.replace(/ :/g, " *:").replace(/::after\b/g, "")));
  let allMatchingStyleRules = [...matchingActiveRules, ...matchingDefaultRules, ...matchingHoverRules, ...matchingBeforeRules, ...matchingAfterRules];
  let matchingAnimationNames = allMatchingStyleRules.map(rule => rule.style.animationName).filter(n => n.trim());
  let matchingKeyframeRules = keyframesRules.filter(rule => matchingAnimationNames.includes(rule.name));
  
  // make name changes before actually grabbing the style text of each type
  allMatchingStyleRules.forEach(rule => rule.style.animationName = rule.style.animationName+uniqueId);
  matchingKeyframeRules.forEach(rule => rule.name = rule.name+uniqueId);

  let matchingDefaultStyles = matchingDefaultRules.map(rule => rule.cssText).map(r => r.split(/[{}]/g)[1].trim()).join(" ") + (el.getAttribute('style') || ""); // important to add these last because inline styles are meant to override stylesheet styles (unless !important is used)
  let matchingHoverStyles = matchingHoverRules.map(rule => rule.cssText).map(r => r.split(/[{}]/g)[1].trim()).join(" ");
  let matchingActiveStyles = matchingActiveRules.map(rule => rule.cssText).map(r => r.split(/[{}]/g)[1].trim()).join(" ");
  let matchingBeforeStyles = matchingBeforeRules.map(rule => rule.cssText).map(r => r.split(/[{}]/g)[1].trim()).join(" ");
  let matchingAfterStyles = matchingAfterRules.map(rule => rule.cssText).map(r => r.split(/[{}]/g)[1].trim()).join(" ");
  let matchingKeyframeStyles = matchingKeyframeRules.map(rule => rule.cssText).join(" ");
  
  // undo the rule name changes because this actually affects the whole document:
  matchingKeyframeRules.forEach(rule => rule.name = rule.name.replace(uniqueId, "")); 
  allMatchingStyleRules.forEach(rule => rule.style.animationName = rule.style.animationName.replace(uniqueId, ""));

  let data = {
    uniqueId,
    defaultStyles: matchingDefaultStyles,
    hoverStyles: matchingHoverStyles,
    activeStyles: matchingActiveStyles,
    keyframeStyles: matchingKeyframeStyles,
    beforeStyles: matchingBeforeStyles,
    afterStyles: matchingAfterStyles,
  }
  return data;
}

The :focus, :focus-within and :visited selectors are not included, but could be easily added.

joe
  • 3,752
  • 1
  • 32
  • 41
0

You can calculate the user-applied (non-default) styles by comparing them against a "default" HTML element of the same tag name, which is rendered in an isolated <iframe> so no styles from the document "leak" into the default element.

This solution is the same as @Just a student, but adds these improvements:

  1. the <iframe> is a hidden HTML element so the user won't see it
  2. for performance, default styles are cached and we wait to clean up the <iframe> until the end when we call removeSandbox
  3. it accounts for inheritance (i.e. if you provide parentElement it will list a style even when the parent sets it and the element overrides it back to the default)
  4. it accounts for situations where a default style's initial value and computed value don't match (for more info, see note [1] in this PR)
// usage:
element = document.querySelector('div');
styles = getUserComputedStyles(element);
styles = getUserComputedStyles(element, parentElement);
// call this method when done to cleanup:
removeSandbox();

function getUserComputedStyles(element, parentElement = null) {
    var defaultStyle = getDefaultStyle(element.tagName);
    var computedStyle = window.getComputedStyle(element);
    var parentStyle =
      parentElement ? window.getComputedStyle(parentElement) : null;
    var styles = {};
    [...computedStyle].forEach(function(name) {
        // If the style does not match the default, or it does not match the
        // parent's, set it. We don't know which styles are inherited from the
        // parent and which aren't, so we have to always check both.
        // This results in some extra default styles being returned, so if you
        // want to avoid this and aren't concerned about omitting styles that
        // the parent set but the `element` overrides back to the default,
        // call `getUserComputedStyles` without a `parentElement`.
        const computedStyleValue = computedStyle[name];
        if (computedStyleValue !== defaultStyle[name] ||
          (parentStyle && computedStyleValue !== parentStyle[name])) {
            styles[name] = computedStyleValue;
        }
    });
    return styles;
}

var removeDefaultStylesTimeoutId = null;
var sandbox = null;
var tagNameDefaultStyles = {};

function getDefaultStyle(tagName) {
    if (tagNameDefaultStyles[tagName]) {
        return tagNameDefaultStyles[tagName];
    }
    if (!sandbox) {
        // Create a hidden sandbox <iframe> element within we can create
        // default HTML elements and query their computed styles. Elements
        // must be rendered in order to query their computed styles. The
        // <iframe> won't render at all with `display: none`, so we have to
        // use `visibility: hidden` with `position: fixed`.
        sandbox = document.createElement('iframe');
        sandbox.style.visibility = 'hidden';
        sandbox.style.position = 'fixed';
        document.body.appendChild(sandbox);
        // Ensure that the iframe is rendered in standard mode
        sandbox.contentWindow.document.write(
          '<!DOCTYPE html><meta charset="UTF-8"><title>sandbox</title><body>');
    }
    var defaultElement = document.createElement(tagName);
    sandbox.contentWindow.document.body.appendChild(defaultElement);
    // Ensure that there is some content, so properties like margin are applied
    defaultElement.textContent = '.';
    var defaultComputedStyle =
      sandbox.contentWindow.getComputedStyle(defaultElement);
    var defaultStyle = {};
    // Copy styles to an object, making sure that 'width' and 'height' are
    // given the default value of 'auto', since their initial value is always
    // 'auto' despite that the default computed value is sometimes an absolute
    // length.
    [...defaultComputedStyle].forEach(function(name) {
        defaultStyle[name] = (name === 'width' || name === 'height')
          ? 'auto' : defaultComputedStyle.getPropertyValue(name);
    });
    sandbox.contentWindow.document.body.removeChild(defaultElement);
    tagNameDefaultStyles[tagName] = defaultStyle;
    return defaultStyle;
}

function removeSandbox() {
    if (!sandbox) {
        return;
    }
    document.body.removeChild(sandbox);
    sandbox = null;
    if (removeDefaultStylesTimeoutId) {
        clearTimeout(removeDefaultStylesTimeoutId);
    }
    removeDefaultStylesTimeoutId = setTimeout(() => {
        removeDefaultStylesTimeoutId = null;
        tagNameDefaultStyles = {};
    }, 20 * 1000);
}

Even with these improvements, for block elements, some default styles are still listed because their initial and computed values don't match, namely width, height, block-size, inset-block, transform-origin, and perspective-origin (see note in #4). This solution in dom-to-image-more (the getUserComputedStyle function) is able to trim away even more of these, although the calculation is slower.

Joseph238
  • 1,174
  • 1
  • 14
  • 22
-1

I've used this function in the past...

function get_style(obj,nam) { //obj = HTML element, nam = style property
  var val = "";
  if(document.defaultView && document.defaultView.getComputedStyle) {
    nam = nam.replace(/[A-Z]/g,function(str) { //convert name into hypenated
      return "-"+str.toLowerCase();
    });
    val = document.defaultView.getComputedStyle(obj,"").getPropertyValue(nam); //get current style
  }
  else if(obj.currentStyle) {
    nam = nam.replace(/\-(\w)/g,function(str,p1) { //convert name into camel case
      return p1.toUpperCase();
    });
    val = obj.currentStyle[nam]; //get current style
  }
  return val;
}

It allows you to pass in the style property as either hypenated (background-color) or camel case (backgroundColor) and replaces it depending on the method it uses.

This cover older browsers as well, even old IE!

Rik Lewis
  • 750
  • 4
  • 10
  • Hmmm... what I want is a function that lists all the styles (and values) which have been applied by the user (inline or stylesheet only). Your function seems to be a kind of "cross-browser" getComputedStyle – Gyum Fox Feb 03 '17 at 13:38
  • Ah yeah, sorry about that. Slightly misread the question, I thought you were trying to get a specific property value. You could loop through a pre-defined list of all styles that you might be interested in, and then call this function, and only list out the ones which are not blank. But I suspect this would be quite inefficient and produce a rather long list. – Rik Lewis Feb 03 '17 at 13:42