Bug 1306054 - Display an indicator on properties with inactive CSS r=rcaliman
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Tue, 07 May 2019 14:58:50 +0000
changeset 532250 ecf6843307fa68c647f15eca2344d9d34f079ce7
parent 532249 ad447cf869e224bc25d024bff668abcd2b949b5a
child 532251 85d5010b19abc7a58ce617aff2a39360fc54eee5
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcaliman
bugs1306054
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1306054 - Display an indicator on properties with inactive CSS r=rcaliman ### Summary of Changes 1. Added `element-style.js::refreshUnusedCssAll()`: - This method refreshes the CSS declarations for every property change and then calls `element-style.js::_updatePropertyUsed()` for each pseudo element. 2. Added `element-style.js::_updatePropertyUsed()`: - This method simply updates the unused CSS HTML for each property that needs it. 3. Added `alerticon-unused.svg`. 4. A tiny bit of tidying in `devtools/client/themes/rules.css`: - Added blank lines before comments. - Changed `0px` to `0`; - Merged both the `#ruleview-class-panel .classes` rules. - Added used and unused CSS styles. - Hooked `InactivePropertyHelper` into `devtools/server/actors/styles.js` 5. `devtools/server/actors/utils/inactive-property-helper.js` contains the actual unused CSS engine. 6. This feature exposed a race condition inside `head.js::assertShowPreviewTooltip()`. It was mousing over an element and sometimes the tooltip emitted "shown" before we added the listener. This is now fixed. ### Try https://treeherder.mozilla.org/#/jobs?repo=try&revision=016f8dc8e05dbaa89bc5a79b822ce23e786d3fc1 Differential Revision: https://phabricator.services.mozilla.com/D26879
devtools/client/inspector/rules/models/element-style.js
devtools/client/inspector/rules/models/rule.js
devtools/client/inspector/rules/models/text-property.js
devtools/client/inspector/rules/views/rule-editor.js
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/jar.mn
devtools/client/locales/en-US/inspector.properties
devtools/client/preferences/devtools-client.js
devtools/client/themes/images/alerticon-unused.svg
devtools/client/themes/rules.css
devtools/server/actors/styles.js
devtools/server/actors/utils/inactive-property-helper.js
devtools/server/actors/utils/moz.build
--- a/devtools/client/inspector/rules/models/element-style.js
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -1,25 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const Services = require("Services");
 const promise = require("promise");
 const Rule = require("devtools/client/inspector/rules/models/rule");
 const UserProperties = require("devtools/client/inspector/rules/models/user-properties");
 const { ELEMENT_STYLE } = require("devtools/shared/specs/styles");
 
 loader.lazyRequireGetter(this, "promiseWarn", "devtools/client/inspector/shared/utils", true);
 loader.lazyRequireGetter(this, "parseDeclarations", "devtools/shared/css/parsing-utils", true);
 loader.lazyRequireGetter(this, "parseNamedDeclarations", "devtools/shared/css/parsing-utils", true);
 loader.lazyRequireGetter(this, "parseSingleValue", "devtools/shared/css/parsing-utils", true);
 loader.lazyRequireGetter(this, "isCssVariable", "devtools/shared/fronts/css-properties", true);
 
+const PREF_INACTIVE_CSS_ENABLED = "devtools.inspector.inactive.css.enabled";
+
 /**
  * ElementStyle is responsible for the following:
  *   Keeps track of which properties are overridden.
  *   Maintains a list of Rule objects for a given element.
  */
 class ElementStyle {
   /**
    * @param  {Element} element
@@ -60,16 +63,23 @@ class ElementStyle {
 
     if (this.ruleView.isNewRulesView) {
       this.pageStyle.on("stylesheet-updated", this.onRefresh);
       this.ruleView.inspector.styleChangeTracker.on("style-changed", this.onRefresh);
       this.ruleView.selection.on("pseudoclass", this.onRefresh);
     }
   }
 
+  get unusedCssEnabled() {
+    if (!this._unusedCssEnabled) {
+      this._unusedCssEnabled = Services.prefs.getBoolPref(PREF_INACTIVE_CSS_ENABLED);
+    }
+    return this._unusedCssEnabled;
+  }
+
   destroy() {
     if (this.destroyed) {
       return;
     }
 
     this.destroyed = true;
 
     for (const rule of this.rules) {
@@ -120,17 +130,17 @@ class ElementStyle {
 
       this.rules = [];
 
       for (const entry of entries) {
         this._maybeAddRule(entry, existingRules);
       }
 
       // Mark overridden computed styles.
-      this.markOverriddenAll();
+      this.onRuleUpdated();
 
       this._sortRulesForPseudoElement();
 
       if (this.ruleView.isNewRulesView) {
         this.subscribeRulesToLocationChange();
       }
 
       // We're done with the previous list of rules.
@@ -237,36 +247,38 @@ class ElementStyle {
       return false;
     }
 
     this.rules.push(rule);
     return true;
   }
 
   /**
-   * Calls markOverridden with all supported pseudo elements
+   * Calls updateDeclarations with all supported pseudo elements
    */
-  markOverriddenAll() {
+  onRuleUpdated() {
     this.variables.clear();
-    this.markOverridden();
+    this.updateDeclarations();
 
     for (const pseudo of this.cssProperties.pseudoElements) {
-      this.markOverridden(pseudo);
+      this.updateDeclarations(pseudo);
     }
   }
 
   /**
-   * Mark the properties listed in this.rules for a given pseudo element
-   * with an overridden flag if an earlier property overrides it.
+   * Mark the declarations for a given pseudo element with an overridden flag if
+   * an earlier property overrides it and update the editor to show it in the
+   * UI. If there is any inactive CSS we also update the editors state to show
+   * the inactive CSS icon.
    *
    * @param  {String} pseudo
    *         Which pseudo element to flag as overridden.
    *         Empty string or undefined will default to no pseudo element.
    */
-  markOverridden(pseudo = "") {
+  updateDeclarations(pseudo = "") {
     // Gather all the text properties applied by these rules, ordered
     // from more- to less-specific. Text properties from keyframes rule are
     // excluded from being marked as overridden since a number of criteria such
     // as time, and animation overlay are required to be check in order to
     // determine if the property is overridden.
     const textProps = [];
     for (const rule of this.rules) {
       if ((rule.matchedSelectors.length > 0 ||
@@ -340,26 +352,32 @@ class ElementStyle {
         taken[computedProp.name] = computedProp;
 
         if (isCssVariable(computedProp.name)) {
           this.variables.set(computedProp.name, computedProp.value);
         }
       }
     }
 
-    // For each TextProperty, mark it overridden if all of its
-    // computed properties are marked overridden. Update the text
-    // property's associated editor, if any. This will clear the
-    // _overriddenDirty state on all computed properties.
+    // For each TextProperty, mark it overridden if all of its computed
+    // properties are marked overridden. Update the text property's associated
+    // editor, if any. This will clear the _overriddenDirty state on all
+    // computed properties. For each editor we also show or hide the inactive
+    // CSS icon as needed.
     for (const textProp of textProps) {
       // _updatePropertyOverridden will return true if the
       // overridden state has changed for the text property.
       if (this._updatePropertyOverridden(textProp)) {
         textProp.updateEditor();
       }
+
+      // For each editor show or hide the inactive CSS icon as needed.
+      if (textProp.editor && this.unusedCssEnabled) {
+        textProp.editor.updatePropertyState();
+      }
     }
   }
 
   /**
    * Adds a new declaration to the rule.
    *
    * @param  {String} ruleId
    *         The id of the Rule to be modified.
@@ -576,30 +594,30 @@ class ElementStyle {
 
       // Remove the old rule and insert the new rule according to where it appears
       // in the list of applied styles.
       this.rules.splice(oldIndex, 1);
       // If the selector no longer matches, then we leave the rule in
       // the same relative position.
       this.rules.splice(newIndex === -1 ? oldIndex : newIndex, 0, newRule);
 
-      // Mark any properties that are overridden according to the new list of rules.
-      this.markOverriddenAll();
+      // Recompute, mark and update the UI for any properties that are
+      // overridden or contain inactive CSS according to the new list of rules.
+      this.onRuleUpdated();
 
       // In order to keep the new rule in place of the old in the rules view, we need
       // to remove the rule again if the rule was inserted to its new index according
       // to the list of applied styles.
       // Note: you might think we would replicate the list-modification logic above,
       // but that is complicated due to the way the UI installs pseudo-element rules
       // and the like.
       if (newIndex !== -1) {
         this.rules.splice(newIndex, 1);
         this.rules.splice(oldIndex, 0, newRule);
       }
-
       this._changed();
     } catch (e) {
       console.error(e);
     }
   }
 
   /**
    * Subscribes all the rules to location changes.
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -303,17 +303,17 @@ class Rule {
   }
 
   /**
    * Helper function for applyProperties that is called when the actor
    * does not support as-authored styles.  Store disabled properties
    * in the element style's store.
    */
   _applyPropertiesNoAuthored(modifications) {
-    this.elementStyle.markOverriddenAll();
+    this.elementStyle.onRuleUpdated();
 
     const disabledProps = [];
 
     for (const prop of this.textProps) {
       if (prop.invisible) {
         continue;
       }
       if (!prop.enabled) {
@@ -414,17 +414,17 @@ class Rule {
           const modifications = this.domRule.startModifyingProperties(
             this.cssProperties);
           modifier(modifications);
           if (this.domRule.canSetRuleText) {
             return this._applyPropertiesAuthored(modifications);
           }
           return this._applyPropertiesNoAuthored(modifications);
         }).then(() => {
-          this.elementStyle.markOverriddenAll();
+          this.elementStyle.onRuleUpdated();
 
           if (resultPromise === this._applyingModifications) {
             this._applyingModifications = null;
             this.elementStyle._changed();
           }
         }).catch(promiseWarn);
 
     this._applyingModifications = resultPromise;
--- a/devtools/client/inspector/rules/models/text-property.js
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -227,16 +227,29 @@ class TextProperty {
     // true.
     if (!this.rule.domRule.declarations[selfIndex]) {
       return true;
     }
 
     return this.rule.domRule.declarations[selfIndex].isValid;
   }
 
+  isUsed() {
+    const selfIndex = this.rule.textProps.indexOf(this);
+    const declarations = this.rule.domRule.declarations;
+
+    // StyleRuleActor's declarations may have a isUsed flag (if the server is the right
+    // version). Just return true if the information is missing.
+    if (!declarations || !declarations[selfIndex] || !declarations[selfIndex].isUsed) {
+      return { used: true };
+    }
+
+    return declarations[selfIndex].isUsed;
+  }
+
   /**
    * Validate the name of this property.
    *
    * @return {Boolean} true if the property name is valid, false otherwise.
    */
   isNameValid() {
     const selfIndex = this.rule.textProps.indexOf(this);
 
--- a/devtools/client/inspector/rules/views/rule-editor.js
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -696,17 +696,17 @@ RuleEditor.prototype = {
       if (newRuleIndex === -1) {
         newRuleIndex = oldIndex;
       }
 
       // Remove the old rule and insert the new rule.
       rules.splice(oldIndex, 1);
       rules.splice(newRuleIndex, 0, newRule);
       elementStyle._changed();
-      elementStyle.markOverriddenAll();
+      elementStyle.onRuleUpdated();
 
       // We install the new editor in place of the old -- you might
       // think we would replicate the list-modification logic above,
       // but that is complicated due to the way the UI installs
       // pseudo-element rules and the like.
       this.element.parentNode.replaceChild(editor.element, this.element);
 
       // Remove highlight for modified selector
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -177,16 +177,21 @@ TextPropertyEditor.prototype = {
     appendText(this.valueContainer, ";");
 
     this.warning = createChild(this.container, "div", {
       class: "ruleview-warning",
       hidden: "",
       title: l10n("rule.warning.title"),
     });
 
+    this.unusedState = createChild(this.container, "div", {
+      class: "ruleview-unused-warning",
+      hidden: "",
+    });
+
     // Filter button that filters for the current property name and is
     // displayed when the property is overridden by another rule.
     this.filterProperty = createChild(this.container, "div", {
       class: "ruleview-overridden-rule-filter",
       hidden: "",
       title: l10n("rule.filterProperty.title"),
     });
 
@@ -591,18 +596,18 @@ TextPropertyEditor.prototype = {
     // - all of the computed properties have defined values. In case the current property
     //   value contains CSS variables, then the computed properties will be missing and we
     //   want to avoid showing them.
     return this.prop.computed.some(c => c.name !== this.prop.name) &&
            !this.prop.computed.every(c => !c.value);
   },
 
   /**
-   * Update the visibility of the enable checkbox, the warning indicator and
-   * the filter property, as well as the overridden state of the property.
+   * Update the visibility of the enable checkbox, the warning indicator, the used
+   * indicator and the filter property, as well as the overridden state of the property.
    */
   updatePropertyState: function() {
     if (this.prop.enabled) {
       this.enable.style.removeProperty("visibility");
       this.enable.setAttribute("checked", "");
     } else {
       this.enable.style.visibility = "visible";
       this.enable.removeAttribute("checked");
@@ -623,16 +628,27 @@ TextPropertyEditor.prototype = {
 
     if (!this.editing &&
         (this.prop.overridden || !this.prop.enabled ||
          !this.prop.isKnownProperty)) {
       this.element.classList.add("ruleview-overridden");
     } else {
       this.element.classList.remove("ruleview-overridden");
     }
+
+    const { used, reasons } = this.prop.isUsed();
+
+    if (this.editing || this.prop.overridden || !this.prop.enabled || used) {
+      this.element.classList.remove("unused");
+      this.unusedState.hidden = true;
+    } else {
+      this.element.classList.add("unused");
+      this.unusedState.title = reasons.join("\n");
+      this.unusedState.hidden = false;
+    }
   },
 
   /**
    * Update the indicator for computed styles. The computed styles themselves
    * are populated on demand, when they become visible.
    */
   _updateComputed: function() {
     this.computed.innerHTML = "";
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -94,16 +94,17 @@ devtools.jar:
     skin/images/copy.svg (themes/images/copy.svg)
     skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg)
     skin/images/performance-details-waterfall.svg (themes/images/performance-details-waterfall.svg)
     skin/images/performance-details-call-tree.svg (themes/images/performance-details-call-tree.svg)
     skin/images/performance-details-flamegraph.svg (themes/images/performance-details-flamegraph.svg)
     skin/breadcrumbs.css (themes/breadcrumbs.css)
     skin/chart.css (themes/chart.css)
     skin/widgets.css (themes/widgets.css)
+    skin/images/alerticon-unused.svg (themes/images/alerticon-unused.svg)
     skin/rules.css (themes/rules.css)
     skin/images/command-paintflashing.svg (themes/images/command-paintflashing.svg)
     skin/images/command-screenshot.svg (themes/images/command-screenshot.svg)
     skin/images/command-responsivemode.svg (themes/images/command-responsivemode.svg)
     skin/images/command-replay.svg (themes/images/command-replay.svg)
     skin/images/command-pick.svg (themes/images/command-pick.svg)
     skin/images/command-pick-accessibility.svg (themes/images/command-pick-accessibility.svg)
     skin/images/command-frames.svg (themes/images/command-frames.svg)
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -483,8 +483,38 @@ markupView.scrollableBadge.label=scroll
 
 # LOCALIZATION NOTE (markupView.scrollableBadge.tooltip): This is the tooltip that is displayed
 # when hovering over badges next to scrollable elements in the inspector.
 markupView.scrollableBadge.tooltip=This element has scrollable overflow.
 
 # LOCALIZATION NOTE (rulePreviewTooltip.noAssociatedRule): This is the text displayed inside
 # the RulePreviewTooltip when a rule cannot be found for a CSS property declaration.
 rulePreviewTooltip.noAssociatedRule=No associated rule
+
+# LOCALIZATION NOTE (rule.inactive.css.not.flex.container): These properties
+# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
+# property is not active. %S will be replaced with a property name.
+rule.inactive.css.not.flex.container=“%S” has no effect on this element since it’s not a flex container (try adding “display:flex” or “display:inline-flex”)
+
+# LOCALIZATION NOTE (rule.inactive.css.not.flex.item): These properties
+# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
+# property is not active. %S will be replaced with a property name.
+rule.inactive.css.not.flex.item=“%S” has no effect on this element since it’s not a flex item (try adding “display:flex” or “display:inline-flex” to the item’s parent)
+
+# LOCALIZATION NOTE (rule.inactive.css.not.grid.container): These properties
+# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
+# property is not active. %S will be replaced with a property name.
+rule.inactive.css.not.grid.container=“%S” has no effect on this element since it’s not a grid container (try adding “display:grid” or “display:inline-grid”)
+
+# LOCALIZATION NOTE (rule.inactive.css.not.grid.item): These properties
+# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
+# property is not active. %S will be replaced with a property name.
+rule.inactive.css.not.grid.item=“%S” has no effect on this element since it’s not a grid item (try adding “display:grid” or “display:inline-grid” to the item’s parent)
+
+# LOCALIZATION NOTE (rule.inactive.css.not.grid.or.flex.item): These properties
+# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
+# property is not active. %S will be replaced with a property name.
+rule.inactive.css.not.grid.or.flex.item=“%S” has no effect on this element since it’s not a grid or flex item (try adding “display:grid”, “display:flex”, “display:inline-grid” or “display:inline-flex” to the item’s parent)
+
+# LOCALIZATION NOTE (rule.inactive.css.not.grid.or.flex.container): These properties
+# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
+# property is not active. %S will be replaced with a property name.
+rule.inactive.css.not.grid.or.flex.container=“%S” has no effect on this element since it’s neither a flex container nor a grid container (try adding “display:grid” or “display:flex”)
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -45,16 +45,22 @@ pref("devtools.inspector.show_pseudo_ele
 // The default size for image preview tooltips in the rule-view/computed-view/markup-view
 pref("devtools.inspector.imagePreviewTooltipSize", 300);
 // Enable user agent style inspection in rule-view
 pref("devtools.inspector.showUserAgentStyles", false);
 // Show all native anonymous content
 pref("devtools.inspector.showAllAnonymousContent", false);
 // Show user agent shadow roots
 pref("devtools.inspector.showUserAgentShadowRoots", false);
+// Enable Inactive CSS detection
+#if defined(NIGHTLY_BUILD)
+pref("devtools.inspector.inactive.css.enabled", true);
+#else
+pref("devtools.inspector.inactive.css.enabled", false);
+#endif
 // Enable the new Rules View
 pref("devtools.inspector.new-rulesview.enabled", false);
 
 // Grid highlighter preferences
 pref("devtools.gridinspector.gridOutlineMaxColumns", 50);
 pref("devtools.gridinspector.gridOutlineMaxRows", 50);
 pref("devtools.gridinspector.showGridAreas", false);
 pref("devtools.gridinspector.showGridLineNumbers", false);
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/alerticon-unused.svg
@@ -0,0 +1,8 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16">
+  <path stroke="context-stroke" fill="none" d="M15.5 8.5C15.5 12.36 12.36 15.5 8.5 15.5C4.63 15.5 1.5 12.36 1.5 8.5C1.5 4.64 4.63 1.5 8.5 1.5C12.36 1.5 15.5 4.64 15.5 8.5Z"/>
+  <path fill="context-fill" d="M8.98 7.47C9.52 7.47 9.96 7.91 9.96 8.45C9.96 9.42 9.96 11.33 9.96 12.29C9.96 12.83 9.52 13.27 8.98 13.27C8.59 13.27 8.4 13.27 8.01 13.27C7.47 13.27 7.03 12.83 7.03 12.29C7.03 11.33 7.03 9.42 7.03 8.45C7.03 7.91 7.47 7.47 8.01 7.47C8.4 7.47 8.59 7.47 8.98 7.47Z"/>
+  <path fill="context-fill" d="M9.96 5.36C9.96 6.16 9.3 6.81 8.49 6.81C7.69 6.81 7.03 6.16 7.03 5.36C7.03 4.57 7.69 3.92 8.49 3.92C9.3 3.92 9.96 4.57 9.96 5.36Z"/>
+</svg>
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -2,38 +2,41 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* CSS Variables specific to this panel that aren't defined by the themes */
 :root {
   --rule-highlight-background-color: var(--theme-highlight-yellow);
   --rule-header-background-color: var(--theme-toolbar-background);
   --rule-pseudo-class-text-color: var(--yellow-70) ;
+
   /* This should be --yellow-50 but since we need an opacity of 0.4, we hard-code the
   resulting color here for now. */
   --rule-property-highlight-background-color: #FFF697;
 }
 
 :root.theme-dark {
   --rule-highlight-background-color: #521C76;
   --rule-header-background-color: #222225;
   --rule-pseudo-class-text-color: var(--yellow-50);
+
   /* This should be --yellow-50 but since we need an opacity of 0.3, we hard-code the
   resulting color here for now. */
   --rule-property-highlight-background-color: #605913;
 }
 
 /* Rule View Tabpanel */
 
 #sidebar-panel-ruleview {
   margin: 0;
   display: flex;
   flex-direction: column;
   width: 100%;
   height: 100%;
+
   /* Override the min-width from .inspector-tabpanel, as the rule panel can support small
      widths */
   min-width: 100px;
 }
 
 /* Rule View Toolbar */
 
 #ruleview-toolbar-container {
@@ -42,17 +45,17 @@
   padding: 0;
 }
 
 #ruleview-toolbar {
   display: flex;
 }
 
 #ruleview-toolbar > .devtools-searchbox:first-child {
-  padding-inline-start: 0px;
+  padding-inline-start: 0;
 }
 
 #ruleview-command-toolbar {
   display: flex;
 }
 
 .ruleview-reveal-panel {
   background: var(--rule-header-background-color);
@@ -186,16 +189,18 @@
   cursor: pointer;
 }
 
 .ruleview-computedlist,
 .ruleview-expandable-container[hidden],
 .ruleview-overridden-items[hidden],
 .ruleview-overridden-rule-filter[hidden],
 .ruleview-warning[hidden],
+.ruleview-unused-warning[hidden],
+.ruleview-used[hidden],
 .ruleview-overridden .ruleview-grid {
   display: none;
 }
 
 .ruleview-computedlist[user-open],
 .ruleview-computedlist[filter-open],
 .ruleview-overridden-items {
   display: block;
@@ -254,17 +259,17 @@
   cursor: pointer;
 }
 
 .ruleview-expandable-header:hover {
   background-color: var(--theme-toolbar-background-hover);
 }
 
 .ruleview-rule-pseudo-element {
-  padding-left:20px;
+  padding-left: 20px;
   border-left: solid 10px;
 }
 
 .ruleview-rule {
   border-bottom: 1px solid var(--theme-splitter-color);
   padding: 2px 4px;
 }
 
@@ -312,16 +317,21 @@
 .ruleview-rule.uneditable .ruleview-propertyvaluecontainer >
 .ruleview-propertyvalue,
 .ruleview-rule[uneditable=true] .ruleview-namecontainer > .ruleview-propertyname,
 .ruleview-rule[uneditable=true] .ruleview-propertyvaluecontainer >
 .ruleview-propertyvalue {
   border-bottom-color: transparent;
 }
 
+.ruleview-property.unused .ruleview-namecontainer,
+.ruleview-property.unused .ruleview-propertyvaluecontainer {
+  opacity: 0.5;
+}
+
 .ruleview-overridden-rule-filter {
   display: inline-block;
   width: 14px;
   height: 14px;
   margin-inline-start: 3px;
   background-image: url(chrome://devtools/skin/images/filter-small.svg);
   background-position: center;
   background-repeat: no-repeat;
@@ -354,27 +364,41 @@
   position: relative;
   float: left;
   left: -38px;
   box-sizing: content-box;
   border-left: 10px solid transparent;
   background-clip: content-box;
 }
 
-.ruleview-warning {
+.ruleview-warning,
+.ruleview-unused-warning {
   display: inline-block;
   width: 12px;
   height: 12px;
   margin-inline-start: 5px;
   background-image: url(chrome://devtools/skin/images/alert.svg);
   background-size: cover;
   -moz-context-properties: fill;
   fill: var(--yellow-60);
 }
 
+.ruleview-unused-warning {
+  background-image: url(chrome://devtools/skin/images/alerticon-unused.svg);
+  background-color: var(--theme-sidebar-background);
+  -moz-context-properties: fill, stroke;
+  fill: var(--theme-icon-dimmed-color);
+  stroke: var(--theme-icon-dimmed-color);
+}
+
+.ruleview-unused-warning:hover {
+  fill: var(--theme-icon-color);
+  stroke: var(--theme-icon-color);
+}
+
 .ruleview-rule:not(:hover) .ruleview-enableproperty {
   visibility: hidden;
 }
 
 .ruleview-expander {
   vertical-align: middle;
 }
 
@@ -413,21 +437,21 @@
   position: relative;
 }
 
 .ruleview-overridden-item::before,
 .ruleview-overridden-item::after {
   content: "";
   position: absolute;
   display: block;
-  border: 0px solid var(--theme-text-color-alt);
+  border: 0 solid var(--theme-text-color-alt);
 }
 
 .ruleview-overridden-item::before {
-  top: 0px;
+  top: 0;
   left: -15px;
   height: 0.8em;
   width: 10px;
   border-left-width: 0.5px;
   border-bottom-width: 0.5px;
 }
 
 .ruleview-overridden-item::after {
@@ -450,16 +474,17 @@
 .ruleview-flex,
 .ruleview-grid,
 .ruleview-shapeswatch,
 .ruleview-swatch {
   cursor: pointer;
   width: 1em;
   height: 1em;
   vertical-align: middle;
+
   /* align the swatch with its value */
   margin-top: -1px;
   margin-inline-end: 5px;
   display: inline-block;
   position: relative;
 }
 
 /* Icon swatches not using the .ruleview-swatch class (flex, grid, shape) */
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -16,16 +16,18 @@ const TrackChangeEmitter = require("devt
 const {pageStyleSpec, styleRuleSpec, ELEMENT_STYLE} = require("devtools/shared/specs/styles");
 
 loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "SharedCssLogic", "devtools/shared/inspector/css-logic");
 loader.lazyRequireGetter(this, "getDefinedGeometryProperties",
   "devtools/server/actors/highlighters/geometry-editor", true);
 loader.lazyRequireGetter(this, "isCssPropertyKnown",
   "devtools/server/actors/css-properties", true);
+loader.lazyRequireGetter(this, "inactivePropertyHelper",
+  "devtools/server/actors/utils/inactive-property-helper", true);
 loader.lazyRequireGetter(this, "parseNamedDeclarations",
   "devtools/shared/css/parsing-utils", true);
 loader.lazyRequireGetter(this, "prettifyCSS",
   "devtools/shared/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "UPDATE_PRESERVING_RULES",
   "devtools/server/actors/stylesheets", true);
 loader.lazyRequireGetter(this, "UPDATE_GENERAL",
   "devtools/server/actors/stylesheets", true);
@@ -1278,24 +1280,28 @@ var StyleRuleActor = protocol.ActorClass
     // and so that we can safely determine if a declaration is valid rather than
     // have the client guess it.
     if (form.authoredText || form.cssText) {
       // authoredText may be an empty string when deleting all properties; it's ok to use.
       const cssText = (typeof form.authoredText === "string")
         ? form.authoredText
         : form.cssText;
       const declarations = parseNamedDeclarations(isCssPropertyKnown, cssText, true);
+      const el = this.pageStyle.cssLogic.viewedElement;
+      const style = this.pageStyle.cssLogic.computedStyle;
 
       // We need to grab CSS from the window, since calling supports() on the
       // one from the current global will fail due to not being an HTML global.
       const CSS = this.pageStyle.inspector.targetActor.window.CSS;
       form.declarations = declarations.map(decl => {
         // Use the 1-arg CSS.supports() call so that we also accept !important
         // in the value.
         decl.isValid = CSS.supports(`${decl.name}:${decl.value}`);
+        decl.isUsed = inactivePropertyHelper.isPropertyUsed(
+          el, style, this.rawRule, decl.name);
         // Check property name. All valid CSS properties support "initial" as a value.
         decl.isNameValid = CSS.supports(decl.name, "initial");
         return decl;
       });
       // Cache parsed declarations so we don't needlessly re-parse authoredText every time
       // we need need to check previous property names and values when tracking changes.
       this._declarations = declarations;
     }
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/utils/inactive-property-helper.js
@@ -0,0 +1,366 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Services = require("Services");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const PREF_UNUSED_CSS_ENABLED = "devtools.inspector.inactive.css.enabled";
+const INSPECTOR_L10N =
+  new LocalizationHelper("devtools/client/locales/inspector.properties");
+
+class InactivePropertyHelper {
+  /**
+   * A list of rules for when CSS properties have no effect.
+   *
+   * In certain situations, CSS properties do not have any effect. A common
+   * example is trying to set a width on an inline element like a <span>.
+   *
+   * There are so many properties in CSS that it's difficult to remember which
+   * ones do and don't apply in certain situations. Some are straight-forward
+   * like `flex-wrap` only applying to an element that has `display:flex`.
+   * Others are less trivial like setting something other than a color on a
+   * `:visited` pseudo-class.
+   *
+   * This file contains "rules" in the form of objects with the following
+   * properties:
+   * {
+   *   invalidProperties (see note):
+   *     Array of CSS property names that are inactive if the rule matches.
+   *   validProperties (see note):
+   *     Array of CSS property names that are active if the rule matches.
+   *   when:
+   *     The rule itself, a JS function used to identify the conditions
+   *     indicating whether a property is valid or not.
+   *
+   *   error:
+   *     A JS function that returns a custom error message explaining why the
+   *     property is inactive in this situation. This function takes a single
+   *     argument: the property name.
+   * }
+   *
+   * NOTE: validProperties and invalidProperties are mutually exclusive.
+   *
+   * The main export is `isPropertyUsed()`, which can be used to check if a
+   * property is used or not, and why.
+   */
+  get VALIDATORS() {
+    return [
+      // Flex container property used on non-flex container.
+      {
+        invalidProperties: [
+          "flex-direction",
+          "flex-flow",
+          "flex-wrap",
+        ],
+        when: () => !this.flexContainer,
+        error: property => msg("rule.inactive.css.not.flex.container", property),
+      },
+      // Flex item property used on non-flex item.
+      {
+        invalidProperties: [
+          "flex",
+          "flex-basis",
+          "flex-grow",
+          "flex-shrink",
+          "order",
+        ],
+        when: () => !this.flexItem,
+        error: property => msg("rule.inactive.css.not.flex.item", property),
+      },
+      // Grid container property used on non-grid container.
+      {
+        invalidProperties: [
+          "grid-auto-columns",
+          "grid-auto-flow",
+          "grid-auto-rows",
+          "grid-template",
+          "grid-gap",
+          "row-gap",
+          "column-gap",
+          "justify-items",
+        ],
+        when: () => !this.gridContainer,
+        error: property => msg("rule.inactive.css.not.grid.container", property),
+      },
+      // Grid item property used on non-grid item.
+      {
+        invalidProperties: [
+          "grid-area",
+          "grid-column",
+          "grid-column-end",
+          "grid-column-start",
+          "grid-row",
+          "grid-row-end",
+          "grid-row-start",
+          "justify-self",
+        ],
+        when: () => !this.gridItem,
+        error: property => msg("rule.inactive.css.not.grid.item", property),
+      },
+      // Grid and flex item properties used on non-grid or non-flex item.
+      {
+        invalidProperties: [
+          "align-self",
+        ],
+        when: () => !this.gridItem && !this.flexItem,
+        error: property => msg("rule.inactive.css.not.grid.or.flex.item", property),
+      },
+      // Grid and flex container properties used on non-grid or non-flex container.
+      {
+        invalidProperties: [
+          "align-content",
+          "align-items",
+          "justify-content",
+        ],
+        when: () => !this.gridContainer && !this.flexContainer,
+        error: property => msg("rule.inactive.css.not.grid.or.flex.container", property),
+      },
+    ];
+  }
+
+  get unusedCssEnabled() {
+    if (!this._unusedCssEnabled) {
+      this._unusedCssEnabled = Services.prefs.getBoolPref(PREF_UNUSED_CSS_ENABLED);
+    }
+    return this._unusedCssEnabled;
+  }
+
+  /**
+   * Is this CSS property having any effect on this element?
+   *
+   * @param {DOMNode} el
+   *        The DOM element.
+   * @param {Style} elStyle
+   *        The computed style for this DOMNode.
+   * @param {DOMRule} cssRule
+   *        The CSS rule the property is defined in.
+   * @param {String} property
+   *        The CSS property name.
+   *
+   * @return {Object} object
+   * @return {Boolean} object.used
+   *         true if the property is used.
+   * @return {Array} object.reasons
+   *         A string array listing the reasons a property isn't used.
+   */
+  isPropertyUsed(el, elStyle, cssRule, property) {
+    if (!this.unusedCssEnabled) {
+      return {used: true};
+    }
+
+    const errors = [];
+
+    this.VALIDATORS.forEach(validator => {
+      // First check if this rule cares about this property.
+      let isRuleConcerned = false;
+
+      if (validator.invalidProperties) {
+        isRuleConcerned = validator.invalidProperties === "*" ||
+                          validator.invalidProperties.includes(property);
+      } else if (validator.validProperties) {
+        isRuleConcerned = !validator.validProperties.includes(property);
+      }
+
+      if (!isRuleConcerned) {
+        return;
+      }
+
+      this.select(el, elStyle, cssRule, property);
+
+      // And then run the validator, gathering the error message if the
+      // validator passes.
+      if (validator.when()) {
+        const error = validator.error(property);
+
+        if (typeof error === "string") {
+          errors.push(validator.error(property));
+        }
+      }
+    });
+
+    return {
+      used: !errors.length,
+      reasons: errors,
+    };
+  }
+
+  /**
+   * Focus on a node.
+   *
+   * @param {DOMNode} node
+   *        Node to focus on.
+   */
+  select(node, style, cssRule, property) {
+    this._node = node;
+    this._cssRule = cssRule;
+    this._property = property;
+    this._style = style;
+  }
+
+  /**
+   * Provide a public reference to node.
+   */
+  get node() {
+    return this._node;
+  }
+
+  /**
+   * Cache and provide node's computed style.
+   */
+  get style() {
+    return this._style;
+  }
+
+  /**
+   * Check if the current node's propName is set to one of the values passed in
+   * the values array.
+   *
+   * @param {String} propName
+   *        Property name to check.
+   * @param {Array} values
+   *        Values to compare against.
+   */
+  checkStyle(propName, values) {
+    return this.checkStyleForNode(this.node, propName, values);
+  }
+
+  /**
+   * Check if a node's propName is set to one of the values passed in the values
+   * array.
+   *
+   * @param {DOMNode} node
+   *        The node to check.
+   * @param {String} propName
+   *        Property name to check.
+   * @param {Array} values
+   *        Values to compare against.
+   */
+  checkStyleForNode(node, propName, values) {
+    return values.some(value => this.style[propName] === value);
+  }
+
+  /**
+   * Check if the current node is a flex container i.e. a node that has a style
+   * of `display:flex` or `display:inline-flex`.
+   */
+  get flexContainer() {
+    return this.checkStyle("display", ["flex", "inline-flex"]);
+  }
+
+  /**
+   * Check if the current node is a flex item.
+   */
+  get flexItem() {
+    return this.isFlexItem(this.node);
+  }
+
+  /**
+   * Check if the current node is a grid container i.e. a node that has a style
+   * of `display:grid` or `display:inline-grid`.
+   */
+  get gridContainer() {
+    return this.checkStyle("display", ["grid", "inline-grid"]);
+  }
+
+  /**
+   * Check if the current node is a grid item.
+   */
+  get gridItem() {
+    return this.isGridItem(this.node);
+  }
+
+  /**
+   * Check if a node is a flex item.
+   *
+   * @param {DOMNode} node
+   *        The node to check.
+   */
+  isFlexItem(node) {
+    return !!node.parentFlexElement;
+  }
+
+  /**
+   * Check if a node is a flex container.
+   *
+   * @param {DOMNode} node
+   *        The node to check.
+   */
+  isFlexContainer(node) {
+    return !!node.getAsFlexContainer();
+  }
+
+  /**
+   * Check if a node is a grid container.
+   *
+   * @param {DOMNode} node
+   *        The node to check.
+   */
+  isGridContainer(node) {
+    return !!node.getGridFragments().length > 0;
+  }
+
+  /**
+   * Check if a node is a grid item.
+   *
+   * @param {DOMNode} node
+   *        The node to check.
+   */
+  isGridItem(node) {
+    return !!this.getParentGridElement(this.node);
+  }
+
+  getParentGridElement(node) {
+    if (node.nodeType === node.ELEMENT_NODE) {
+      const display = this.style.display;
+
+      if (!display || display === "none" || display === "contents") {
+        // Doesn't generate a box, not a grid item.
+        return null;
+      }
+      const position = this.style.position;
+      if (position === "absolute" ||
+          position === "fixed" ||
+          this.style.cssFloat !== "none") {
+        // Out of flow, not a grid item.
+        return null;
+      }
+    } else if (node.nodeType !== node.TEXT_NODE) {
+      return null;
+    }
+
+    for (let p = node.flattenedTreeParentNode; p; p = p.flattenedTreeParentNode) {
+      const style = node.ownerGlobal.getComputedStyle(p);
+      const display = style.display;
+
+      if (display.includes("grid") && !!p.getGridFragments().length > 0) {
+        // It's a grid item!
+        return p;
+      }
+      if (display !== "contents") {
+        return null; // Not a grid item, for sure.
+      }
+      // display: contents, walk to the parent
+    }
+    return null;
+  }
+}
+
+/**
+ * Helper function that gets localized strings.
+ *
+ * @param  {String} propName
+ *         The property name to use. This property name must exist in the
+ *         `inspector.properties` file).
+ * @param  {*} values
+ *         Values to be used as replacement strings.
+ */
+function msg(...args) {
+  return INSPECTOR_L10N.getFormatStr(...args);
+}
+
+exports.inactivePropertyHelper = new InactivePropertyHelper();
--- a/devtools/server/actors/utils/moz.build
+++ b/devtools/server/actors/utils/moz.build
@@ -8,15 +8,16 @@ DevToolsModules(
     'accessibility.js',
     'actor-registry-utils.js',
     'actor-registry.js',
     'breakpoint-actor-map.js',
     'css-grid-utils.js',
     'dbg-source.js',
     'event-breakpoints.js',
     'event-loop.js',
+    'inactive-property-helper.js',
     'make-debugger.js',
     'shapes-utils.js',
     'stack.js',
     'TabSources.js',
     'track-change-emitter.js',
     'walker-search.js',
 )