Skip to content
Advertisement

Get the css definition of an element

Is there a fast way to retrieve the CSS width definition (or any other CSS attribute) of an element?

Notes

  1. I need to know the actual definition, like 80% or 42vw, and NOT the calculated pixel dimension.
  2. The width might be set on different CSS selectors, as illustrated in the code below

Here is a sample of the markup/CSS I have:

body div > section { 
    width: 30% 
}
#content section { 
    width: auto 
}
first { 
    width: 75% !important
}
<body>
  <div id="content">
    <section class="first"> ... </section>
  </div>
</body>

Problem: I want to find out via JS, that the <section> has a width of 75%.

Unfortunately, getMatchedCSSRules() is exactly what I need, but it’s deprecated and already removed from browsers.

Advertisement

Answer

I found a way to determine the exact CSS definition of any element by inspecting the document.styleSheets collection.

Getting accurate results was quite complex, as the result needs to respect (a) media queries that match the current viewport and (b) evaluate the CSS specificity of selectors.

Here is a complete function that’s optimized for high performance – finding a single property among 40+ CSS files with a total of 15k+ of individual rules took 40-50ms

Here is the gist (might be more current than the code below)

/**
 * Scans all CSS rules in the current document to find the most
 * specific definiton of a single CSS property for a given element.
 *
 * Usage: getStyleDef('#my-element', 'width'); 
 *  --> returns the most specific "width" defintiion, e.g. "27em".
 *
 * @param {HTMLElement} element - The HTML Element to inspect.
 * @param {string} prop - The CSS property to inspect.
 * @return {string} The most specific CSS definition, 
 *         or an empty string.
 */
function getStyleDef(element, prop) {
    let result = '';
    let highestSpecificity = false;
    const allRules = [];
    const relevantRules = [];

    // Translate a selector string into an HTMLElement.
    if ('string' === typeof element) {
        element = document.querySelector(element);
    }

    // Translate a jQuery element to a HTMLElement.
    if (element && element instanceof jQuery) {
        element = element.get(0);
    }

    // Bail, if no valid element is specified.
    if (!element || !(element instanceof HTMLElement)) {
        return '';
    }

    // When an inline style is set, always return it.
    if (element.style && '' !== element.style[prop]) {
        return element.style[prop];
    }

    // Parse all stylesheet entries to find relevant CSS rules.
    _getRelevantRules(element);

    // Loop all relevant rules to find the most specific one.
    for (let i = relevantRules.length - 1; i >= 0; i--) {
        // Is there a rule for the required property?
        if ('' === relevantRules[i].style[prop]) {
            continue;
        }

        // Determine the specificity of the given CSS selector.
        const specificity = _getCssSpecificity(
            relevantRules[i].selectorText,
            relevantRules[i].style[prop]
        );

        // Find the most specific CSS definition.
        if (_compareCssSpecificity(specificity, highestSpecificity) > 0) {
            highestSpecificity = specificity;
            result = relevantRules[i].style[prop];
        }
    }

    return result;

    // -- Helper functions follow --

    /**
     * Convert an array-like object to array.
     *
     * @param {Iterable} list - A value-list that can be converted to an array.
     * @return {array} An array representation of the list, or an empty array.
     */
    function _toArray(list) {
        if ('undefined' === typeof list || null === list) {
            return [];
        }
        try {
            if ('function' === typeof Array.from) {
                return Array.from(list);
            } else {
                return [].slice.call(list);
            }
        } catch (exception) {
            // In case the list cannot be converted, return an empty array.
            return [];
        }
    }

    /**
     * Handles extraction of `cssRules` as an `Array` from a stylesheet
     * or something that behaves the same.
     *
     * The rules are prepended to the allRules array. This function does
     * not return a value.
     *
     * @param {CSSStyleSheet} stylesheet - The stylesheet to parse.
     */
    function _extractSheetRules(stylesheet) {
        try {
            // Skip disabled rules.
            if (stylesheet.disabled) {
                return;
            }

            // Skip rules that do not match the current viewport.
            const media = stylesheet.media;
            if (media && !matchMedia(media.mediaText).matches) {
                return;
            }

            // Prepend the rules to the `allRules` array.
            Array.prototype.unshift.apply(allRules, _toArray(stylesheet.cssRules));
        } catch (exception) {
            /*
             * CORS prevents us from accessing rules from other domains, such
             * as google font styles.
             */
        }
    }

    /**
     * Parses all stylesheets and populates the `relevantRules` array with CSS
     * rules that match the given element AND the current viewport size.
     *
     * @param {HTMLElement} element - The element to inspect.
     * @private
     */
    function _getRelevantRules(element) {
        let rule;

        // assuming the browser hands us stylesheets in order of appearance
        // we iterate them from the beginning to follow proper cascade order
        for (let i = 0; i < document.styleSheets.length; i++) {

            // Extract the style rules of this sheet into `allRules`.
            _extractSheetRules(document.styleSheets[i]);

            // Loop the rules in order of appearance.
            while (rule = allRules.shift()) {
                if (rule.styleSheet) {
                    // Insert the `@import`ed stylesheet's rules at the
                    // beginning of this stylesheet's rules.
                    _extractSheetRules(rule.styleSheet);

                    // ... and skip the rest of this rule.
                    continue;
                }

                // If there's no stylesheet attribute BUT there IS a
                // media attribute it's a media rule.
                if (rule.media) {
                    // insert the contained rules of this media rule to
                    // the beginning of this stylesheet's rules.
                    _extractSheetRules(rule);

                    // ... and skip the rest it.
                    continue;
                }

                // check if this element matches this rule's selector
                if (element.matches(rule.selectorText)) {
                    // push the rule to the results set
                    relevantRules.push(rule);
                }
            }
        }
    }

    /**
     *  Calculates the CSS specificity of a CSS selector.
     *
     *  @param input - The CSS selector.
     *  @param attribValue - Optional. A specific CSS attribute value. Only used
     *         to properly recognize `!important` values.
     *  @return {array} Always an array with 4 elements.
     */
    function _getCssSpecificity(input, attribValue) {
        const token = input.split(',');

        if (token.length > 1) {
            let result = [];
            let singleSpecificity;

            for (let i = 0; i < token.length; i++) {
                singleSpecificity = _getCssSpecificity(token[i]);
                if (_compareCssSpecificity(singleSpecificity, result) > 0) {
                    result = singleSpecificity;
                }
            }

            return result;
        }

        let selector = input,
            findMatch,
            typeCount = {
                'a': 0,
                'b': 0,
                'c': 0
            },
            // The following regular expressions assume that selectors matching the
            // preceding regular expressions have been removed.
            attributeRegex = /([[^]]+])/g,
            idRegex = /(#[^#s+>~.[:)]+)/g,
            classRegex = /(.[^s+>~.[:)]+)/g,
            pseudoElementRegex = /(::[^s+>~.[:]+|:first-line|:first-letter|:before|:after)/gi,
            // A regex for pseudo classes with brackets - :nth-child(),
            // :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang()
            // The negation pseudo class (:not) is filtered out because specificity
            // is calculated on its argument.
            // :global and :local are filtered out - they look like pseudo classes
            // but are an identifier for CSS Modules.
            pseudoClassWithBracketsRegex = /(:(?!not|global|local)[w-]+([^)]*))/gi,
            // A regex for other pseudo classes, which don't have brackets
            pseudoClassRegex = /(:(?!not|global|local)[^s+>~.[:]+)/g,
            elementRegex = /([^s+>~.[:]+)/g,
            isImportant;

        isImportant = 'string' === typeof attribValue && attribValue.indexOf('!important') > 0;

        // Find matches for a regular expression in a string and push their details
        // to parts. Type is "a" for IDs, "b" for classes, attributes and pseudo-
        // classes and "c" for elements and pseudo-elements.
        findMatch = function (regex, type) {
            let matches, i, len, match, index, length;
            if (regex.test(selector)) {
                matches = selector.match(regex);
                for (i = 0, len = matches.length; i < len; i += 1) {
                    typeCount[type] += 1;
                    match = matches[i];
                    index = selector.indexOf(match);
                    length = match.length;
                    // Replace this simple selector with whitespace so it won't be
                    // counted in further simple selectors.
                    selector = selector.replace(match, Array(length + 1).join(' '));
                }
            }
        };

        // Replace escaped characters with plain text, using the "A" character
        // https://www.w3.org/TR/CSS21/syndata.html#characters
        (function () {
            const replaceWithPlainText = function (regex) {
                    let matches, i, len, match;

                    if (regex.test(selector)) {
                        matches = selector.match(regex);
                        for (i = 0, len = matches.length; i < len; i += 1) {
                            match = matches[i];
                            selector = selector.replace(match, Array(match.length + 1).join('A'));
                        }
                    }
                },
                // Matches a backslash followed by six hexadecimal digits followed
                // by an optional single whitespace character.
                escapeHexadecimalRegex = /\[0-9A-Fa-f]{6}s?/g,
                // Matches a backslash followed by fewer than six hexadecimal digits
                // followed by a mandatory single whitespace character.
                escapeHexadecimalRegex2 = /\[0-9A-Fa-f]{1,5}s/g,
                // Matches a backslash followed by any character.
                escapeSpecialCharacter = /\./g;

            replaceWithPlainText(escapeHexadecimalRegex);
            replaceWithPlainText(escapeHexadecimalRegex2);
            replaceWithPlainText(escapeSpecialCharacter);
        }());

        // Remove anything after a left brace in case a user has pasted in a rule, not just a
        // selector
        (function () {
            let regex = /{[^]*/gm,
                matches, i, len, match;

            if (regex.test(selector)) {
                matches = selector.match(regex);
                for (i = 0, len = matches.length; i < len; i += 1) {
                    match = matches[i];
                    selector = selector.replace(match, Array(match.length + 1).join(' '));
                }
            }
        }());

        // Add attribute selectors to parts collection (type b)
        findMatch(attributeRegex, 'b');

        // Add ID selectors to parts collection (type a)
        findMatch(idRegex, 'a');

        // Add class selectors to parts collection (type b)
        findMatch(classRegex, 'b');

        // Add pseudo-element selectors to parts collection (type c)
        findMatch(pseudoElementRegex, 'c');

        // Add pseudo-class selectors to parts collection (type b)
        findMatch(pseudoClassWithBracketsRegex, 'b');
        findMatch(pseudoClassRegex, 'b');

        // Remove universal selector and separator characters
        selector = selector.replace(/[*s+>~]/g, ' ');

        // Remove any stray dots or hashes which aren't attached to words
        // These may be present if the user is live-editing this selector
        selector = selector.replace(/[#.]/g, ' ');

        // Remove the negation pseudo-class (:not) but leave its argument because
        // specificity is calculated on its argument. Remove non-standard :local and
        // :global CSS Module identifiers because they do not effect the specificity.
        selector = selector.replace(/:not/g, '    ');
        selector = selector.replace(/:local/g, '      ');
        selector = selector.replace(/:global/g, '       ');
        selector = selector.replace(/[()]/g, ' ');

        // The only things left should be element selectors (type c)
        findMatch(elementRegex, 'c');

        return [isImportant ? 1 : 0, typeCount.a, typeCount.b, typeCount.c];
    }

    /**
     * Compares two CSS Specificity terms to determine, which one is more
     * specific.
     *
     * @param a - The first term
     * @param b - The second term.
     * @returns {number} 0 if both are equally specific. +1 if a is more
     *          specific, -1 if b is more specific.
     */
    function _compareCssSpecificity(a, b) {
        for (let i = 0; i < 4; i += 1) {
            const valA = parseInt(isNaN(a[i]) ? 0 : a[i]);
            const valB = parseInt(isNaN(b[i]) ? 0 : b[i]);

            if (valA < valB) {
                return -1;
            } else if (valA > valB) {
                return 1;
            }
        }

        return 0;
    }
}
User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement