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
One-page overview
Summary
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.
Platforms
All
Team
chrome-devtools-staff@google.com
Tracking issue
Value proposition
Debugging complicated CSS property values is a recurring request. The complexity of values is growing with new
Code affected
“DevTools front-end”, “Blink”
Signed off by
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 |
Terminology
User stories
Developer goals
As a front-end developer, I want to...
- see the full definition chain of CSS variables, so that I don't need to manually click through multiple variable links in the Styles tab.
- visualize the step-by-step evaluation of complex CSS calculations, so I can identify bugs more efficiently and gain a deeper understanding of how a value is computed.
Iteration 1: improved CSS value tooltip
Seeing the definition chain of CSS variables
- In the Styles tab, the developer hovers over a CSS variable in the Styles tab. A tooltip appears showing the full definition chain down to the computed value.
Visualizing the evaluation of CSS calculations
- In the Styles tab, the developer hovers over a CSS variable or a CSS calculation keyword like calc(), clamp(), min() or max(). A tooltip appears showing the calculation with substituted variables and substituted relative values plus the result of the calculation.
- If the calculation has nested calculations, an expand arrow is shown in front of the calculation.
- A click on the expand arrow reveals the step-by-step evaluation.
- Hovering over a value highlights corresponding parts of the calculation in the steps above or below it.
- This pattern is also used if CSS calculations are part of a definition chain.
Seeing computed values in the Computed tab
- In the Computed tab, variables that have a calculation as a value show the computed value as the final computed step instead of just the calculation. Right now only properties show the final value, not variables (only if they are of type <length>).
Iteration 2: CSS debugger panel
Debugging CSS calculations in a dedicated panel
- In the second iteration, we're adding an expand icon in the top right corner of the tooltip, if a CSS calculation is present.
- Clicking this icon reveals the CSS debugger panel and opens the current calculation as a tab in this panel. Viewing the calculation in a dedicated panel has the benefits of persistence and allows developers to click through variables without losing the tooltip. The calculation is read-only and can't be edited.
- Similar to the tooltip, the CSS debugger panel highlights corresponding values or calculations in the steps above or below it when hovering a value.
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.
Milestones
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 |
Design
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>
Substitutions
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.
Replacing relative units
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.
- An API to break apart a shorthand. While we could conceptually do that in the frontend, adopting all existing shorthand syntaxes and keeping up with future additions and modifications incurs a maintenance burden we can avoid by passing this on to blink. The API will have the following shape:
command getLonghandProperties
parameters
string shorthandName
string value
returns
array of CSSProperty longhandProperties
- An API to resolve relative units. This will reuse the expression evaluation API below.
Evaluate Expressions
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
Accessibility
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.
Risks
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).