Debugging CSS Values
Attention: Externally visible, non-confidential
Author: pfaffe@chromium.orgPeter Müller
Status: Accepted
Short Link: go/chrome-devtools:debugging-css-values-design
Created: 2024-10-16 / Last Updated: 2025-01-22
We propose building a "CSS value debugger" which allows developers to closely inspect how a css value was computed, including for instance which CSS custom properties were used or what the intermediate results of arithmetic computations were.
All
chrome-devtools-staff@google.com
Debugging complicated CSS property values is a recurring request. The complexity of values is growing with new
“DevTools front-end”, “Blink”
Name | Write (not) LGTM in this column |
Danil Somsikov (DevTools TL) | LGTM |
LGTM | |
Penelope McLachlan (PM) | LGTM |
Anders Hartvoll Ruud (Blink) | LGTM |
Peter Müller (UX) | LGTM |
Benedikt Meurer (DevTools EM) | LGTM |
Developer goals
As a front-end developer, I want to...
Seeing the definition chain of CSS variables
Visualizing the evaluation of CSS calculations
Seeing computed values in the Computed tab
Debugging CSS calculations in a dedicated panel
Right now, this dedicated panel has the same functionality as the tooltip. However, in the future, we need a larger UI surface to debug CSS functions. For them we (probably) need a stack, showing the current value of variables and some UI to step through a function.
Project | Status | Notes |
Finish UX design(s) | Launched | |
Backend APIs | Launched | |
Use new APIs to resolve feature requests for the styles tab | Launched | https://crrev.com/c/6191367, https://crrev.com/c/6193852, https://crrev.com/c/6198318 |
Implement value debugger tooltip | In progress | |
Implement dedicated debugger tab | Not started |
Breaking down the computation of CSS values happens in three phases: We first substitute CSS variables and related entities, then we replace relative units with absolute values, and finally we evaluate arithmetic (sub-)expressions. Below, we explain the steps in detail using the following running example, assuming we're inspecting the margin property on the <p> element. One important thing to note is that the custom property --a is registered, giving it a <length> type.
<style>
div {
font-size: 100pt;
--offset: 50%;
--a: calc(var(--offset) + 1em);
--b: calc(var(--offset) + 1em);
}
p {
font-size: 40pt;
--offset: 10%;
margin: var(--a) var(--b);
}
@property --a {
syntax: "<length>";
initial-value: 0px;
inherits: true;
}
</style>
<div><p>Inspect me</p></div>
In the first step, we inline all substitutions. Mainly these are var() values, but in the future, once these features are available, it will also include, e.g., attr() and if(). For brevity we'll just refer to var() values in the section, but everything we describe applies to other substitutions just the same.
Inlining substitutions is an iterative process. We start with the original CSS property value and replace all var() expressions with either the declared value of the referenced custom property or the fallback value text if the reference is undefined (Note that this is different from the actual CSS engine behavior, which inlines computed values and not declared values). If the replacement contains more var() expressions, these are replaced in the next step, and so on until not substitutions remain.
This process will be implemented entirely in devtools-frontend. It already has the existing machinery to resolve var() references and apply the inlining. The downside of that is that making future substitutions work (like attr(), if(), or custom functions) requires implementing support in devtools-frontend. If we had blink facilities to apply these substitutions we could save ourselves that effort. Unfortunately, blink is unable to follow variable declarations across inheritance boundaries, owed to the fact that this behavior actually deviates from the true cascade behavior.
The substitution steps for the running example look like this:
margin: var(--a) var(--b)
<=> calc(var(--offset) + 1em) calc(var(--offset) + 1em)
<=> calc(50% + 1em) calc(50% + 1em)
Note how we inlined the declaration values for --a and --b. Since both properties are inherited this shows the key distinction between how devtools' and blink's var() replacements work. In blink, the computed value would have been used instead, which for --a is some px value given that it's typed as a <length>. To help developers understand clearly how values came together, however, we want to show them the declared values in-context, which blinks resolver cannot provide.
That being said, we cannot entirely ignore the fact that in reality the computed values are propagated as can be seen in how --offset is inlined. In reality var()s are replaced before values are inherited, so when we replace the functions we need to resolve the values in the context of the correct inherited cascade. Devtools already knows how to do that since it's required for the custom property resolutions in the Styles tab.
In the next phase, we replace all relative units with their absolute values. We're introducing this phase before evaluating the arithmetic (sub)expressions in order to reduce some of the confusion that will inevitably arise from the inherit-computed-values behavior. Although technically the entire calc() expression corresponding to --a is evaluated in the context of the <div>, the context is only relevant to the relative units in the expression. We believe it's easier to connect the units to the context in one's mental model than the arithmetic.
Substitution of the units in our example could look like this:
calc(50% + 1em) calc(50% + 1em)
<=> calc(100px + 132px) calc(100px + 54px)
This is assuming that <p>'s parent (i.e., the <div>) is square with an edge length of 100px, that 1em at font-size 100pt is 132px, and that 1em at font-size 40pt is 54px.
To compute absolute unit values a blink API is required since devtools simply does not have sufficient information to easily do the conversion. For most units, conversion in blink is relatively straightforward since only the context element is required (along with the pseudo id and VT name, if applicable). Percentages are a bigger challenge, since what a percentage value is relative to depends on the concrete CSS property, and potentially also where in the property value the percentage appears. In the example above with margin being a shorthand, the percentages refer to the height and width of the container, respectively. In a color property, in an lch value for example, percentages are even actually "absolute" in that sense.
To facilitate this, two separate blink APIs will be added.
command getLonghandProperties
parameters
string shorthandName
string value
returns
array of CSSProperty longhandProperties
With all relative units substituted, we finally evaluate arithmetic expressions step-by-step, starting with the innermost subexpressions. Arithmetic expressions are calls to functions such as calc(), sin(), tan(), or min(), to name a few.
calc(100px + 132px) calc(100px + 54px)
<=> 232px 154px
The API is straightforward:
command resolveValues
parameters
array of string expression
# When resolving percentages, property name defines what they are relative to
optional string propertyName
# Id of the node in whose context the expression is evaluated
DOM.NodeId nodeId
# Pseudo element type.
optional DOM.PseudoType pseudoType
# Pseudo element custom ident.
optional string pseudoIdentifier
returns
# Returns one result per input expression. If an expression is invalid (failes
# to parse or isn't valid for the CSS syntax for the given property name), an
# empty string is returned.
array of string result
Tooltips in the styles tab are not currently keyboard accessible in general. We propose the following change to the styles tab’s keyboard navigation so that tooltips can be accessed via keyboard and can be announced by a screenreader.
Currently, by pressing tab, users can step through the individual selectors, property names, and property values, each time entering editing mode immediately. The up and down arrows step over the list of rules. We want to retain this behavior.
We propose treating the rules navigation less like a list and more like a tree. Up and down arrows step over the rules, right arrow “enters” the rule and selects the first property name. From there, left and right arrows step over the “pieces” of the property, which are the property name and the parts of the value that can be interacted with, such as color swatches or variable links. Pressing Enter acts like a mouse click, e.g. to open the color picker.
When a value part is selected that has a tooltip attached, a hotkey opens the tooltip and focuses the first interactable element within it. We could use Ctrl+K, Ctrl+I to align with vscode.
The value debugger UX poses the largest risk to this project. The proposed UI is dense, the information displayed has subtle nuances to it. In particular the idea to inline variable declared values instead of computed values could cause friction or confusion. We will iterate on the UX with users to ensure what we're building is helpful.
The blink APIs we're proposing are helpful independent of the value debugger UX. There are multiple feature requests for the styles tab where these APIs are key building blocks (e.g., showing the result of a calc, showing longhands in the presence of vars, showing which argument is taken in a min, showing absolute values of units).