Skip to content

Instantly share code, notes, and snippets.

@sliminality
Last active December 10, 2023 04:30
Show Gist options
  • Save sliminality/c44df66647fe9a252b0064d3259fde74 to your computer and use it in GitHub Desktop.
Save sliminality/c44df66647fe9a252b0064d3259fde74 to your computer and use it in GitHub Desktop.
Hidden dependencies in CSS

Hidden dependencies

Sarah Lim, Northwestern University

Update, September 2019: This Gist detailed an early idea which formed the basis for a major research project. For more information, you can read the resulting full paper, which received Best Paper Honorable Mention at UIST 2018. A tool based on this research is now available on Firefox Nightly.

What are hidden dependencies?

Hidden dependencies between CSS properties are a common source of beginner confusion. For instance, a user might write the following code in order to vertically align some text within a <div>:

<div class="container">
  Here is some text, which should be vertically aligned.
</div>
.container {
  height: 100px;  /* larger than the auto text height */
  vertical-align: middle;
}

Unfortunately, writing vertical-align: middle; doesn't do anything: the page remains unchanged, with or without this property. However, adding the hidden dependency display: table-cell; makes the page appear as expected:

.container {
  height: 100px;
  vertical-align: middle;
  display: table-cell;  /* now the text is vertically centered */
}

This solution isn't necessarily the best way to vertically align text, but it does illustrate a common pitfall for CSS beginners: hidden dependencies between properties on a particular element (and sometimes two different elements, such as a parent and child).

To make matters worse, many properties with hidden dependencies have descriptive names, such as vertical-align, z-index, justify-content, width. These implied visual effects lull beginners into a false sense of security: one might reasonably conclude that setting width: 50%; will resize a box of text, only to write

<span class="text">Resize me to 50%!</span>
.text {
  width: 50%;
}

and experience frustration when the CSS has no effect. Here, our problem is that <span> tags have a default computed value of display: inline;, thus width has no effect. Stated otherwise, there is a hidden dependency between the width property and a value of display: block | inline-block; (or the element could also be replaced, but that's a separate case).

While experienced developers have internalized common hidden dependencies through years of trial-and-error, acquiring this knowledge is daunting to beginners: the spec is rarely useful for gaining intuition into the 80% of cases, and online wisdom tends to be dispersed across disjoint sources. In the meantime, beginners struggle to construct accurate mental models for basic CSS functionality, often chalking up styling to "wizardry" or "hacks" (which is a fair assessment in many cases, but not all of them).

Research aims

Current tools offer little support to developers debugging ineffective CSS. Our research aims to improve this situation, by developing techniques and interfaces for inferring and presenting hidden dependencies. (For those curious, you can read about our core mechanism, visual regression pruning. We've since made significant progress generalizing the technique to more interesting applications, but the core concept remains the same.)

As one example of how such blue-sky tooling might work, imagine a beginning CSS developer who wants to stack two elements in an overlapping fashion.

  1. They Google "stack elements CSS," and see z-index in the title of a top result. They vaguely recall seeing z-index: -1; declared somewhere before, so they go back to their editor and add that line.
  2. The page output does not change.
  3. Detecting a file edit with no consequence, two things happen:
    1. The editor analyzes the change and detects the "z-index" property was added, with a grammatically valid value.
    2. The browser looks at the computed styles for the selected element, and notices that the requisite dependency position: absolute | relative | fixed | sticky; is not satisfied.
  4. In conjunction, the tools suggest adding position: relative; to the source code.

While we aren't there yet, this kind of supportive tooling is exactly what our research aims to provide.

Common hidden dependencies

What follows is a very incomplete list of common hidden dependencies. Each corresponds to an empirical learning barrier encountered by multiple novice CSS developers in our lab studies. We have chosen to focus on this subset of examples while prototyping tools for making dependencies explicit, because they correspond to frequently-desired styling outcomes in practice.

You can help in two ways:

  1. Suggest new dependencies, with a particular focus on those which have tripped you up personally sometime over your career, or those you've encountered with reasonable frequency (e.g. on StackOverflow, while mentoring interns, etc.).
    • In this stage of our research, we are particularly interested in hidden dependencies for properties unrelated to layout or the box model. That is, which properties alter the visual appearance of an element without changing its size, position, etc., and what are some hidden dependencies for those properties?
  2. Provide corrections and elaborations to the listed dependencies, many of which are simplified to convey general ideas, leaving out edge cases (or designating them as such).

Positioning

  • z-index depends on ONE of:
    • position: absolute | relative | fixed | sticky; applied to the same element
    • display: flex; applied to the parent element
  • top depends on:
    • position: absolute | relative | fixed | sticky; applied to the same element
  • left depends on:
    • position: absolute | relative | fixed | sticky; applied to the same element
    • Edge cases:
      • If right is specified: direction: ltr; applied to the same element
  • bottom depends on:
    • position: absolute | relative | fixed | sticky; applied to the same element
    • Edge cases:
      • If top is specified: height: auto | 100%; applied to the same element
  • right depends on:
    • position: absolute | relative | fixed | sticky; applied to the same element
    • Edge cases:
      • If left is specified: direction: rtl; applied to the same element

Corollary: If an element has position: relative; and no other dependent positioning modifiers (top, left, z-index, etc.), it very likely serves as a positioned ancestor for another positioned descendant.

  • position: sticky; requires position: relative; and thus does not apply to <thead> or <tr>, only <th> (source)

Alignment

  • vertical-align depends on:
    • display: table-cell; applied to the same element
    • UNLESS display: flex; is applied to the parent element
  • margin: 0 auto; (to center an element) depends on:
    • NOT having float: left | right; applied to the same element
    • NOT having display: flex | inline-flex; applied to the same element
      • This is a slightly more complex edge case, omitted for simplicity

Flexbox

  • flex-direction, flex-row, flex-wrap, justify-content, align-content, align-items depend on:
    • display: flex | inline-flex; applied to the same element
  • flex, flex-grow, flex-shrink, align-self, order depend on:
    • display: flex | inline-flex; applied to the parent element

Dimensions and sizing

  • height depends on:
    • NOT having min-height applied to the same element
    • NOT having max-height applied to the same element
    • NOT being display: inline | table-*; (this is a simplification, to avoid explaining non-replaced elements)
    • If percentage value: height applied to the parent element (or the root element, if position: absolute; is applied to the same element)
  • width depends on:
    • NOT having min-width applied to the same element
    • NOT having max-width applied to the same element
    • NOT being display: inline | table-*; (this is a simplification, to avoid explaining non-replaced elements)
  • (from Josh Comeau) margin-bottom and margin-top depend on:
    • display: block | inline-block; applied to the same element
  • (from Emily Eisenberg) text-overflow: ellipsis depends on
    • width constrained (in pixels), and evaluated (i.e. display: inline-block | block also set)
    • overflow: hidden applied to the same element
    • white-space: nowrap applied to the same element

To explore

  • willChange: transform can break z-ordering/parenting (from Kevin Barabash)
  • position: absolute depends on position of parent element/(nearest positioned ancestor) (from Kevin Barabash)
  • transform can break z-index, position: fixed (from Josh Comeau)
  • overflow, overflow-x, overflow-y (from Michail Yasonik)
  • background-attachment, background-repeat and background-position depend on a background-image (from Michail Yasonik)
  • CSS Grid (from Michail Yasonik)

Epilogue

@kevinbarabash
Copy link

The behavior of position: absolute depends on the what the position of the parent is, e.g. if the parent is position: relative than the child will be absolutely positioned relative to the container instead of the page.

@joshwcomeau
Copy link

This is an awesome idea!! Such a common pain point.

I thought of one other example: margin-top and margin-bottom don't work on display: inline elements. Fiddle

Another thing, although it's less of a beginner trip-up concern, is how applying CSS transforms break certain things like z-index or position: fixed

@kevinbarabash
Copy link

Following up on that using willChange: transform can affect the z-ordering/parenting of things it is often used by browsers as a hint to promote the element to its own layer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment