Gyum Fox
Gyum Fox

Reputation: 3627

How to get the applied style from an element, excluding the default user agent styles

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 :)

Upvotes: 11

Views: 4260

Answers (5)

Joseph238
Joseph238

Reputation: 1224

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.

Upvotes: 0

joe
joe

Reputation: 5552

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.

Upvotes: 3

Just a student
Just a student

Reputation: 11040

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>

Upvotes: 5

er-han
er-han

Reputation: 1921

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>

Upvotes: 12

Rik Lewis
Rik Lewis

Reputation: 749

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!

Upvotes: -1

Related Questions