A CSS-only layout debugger
Tags: css, webdev
Published: 2019-09-16 14:53:40

_Cross-posted from [my personal blog](http://danburzo.ro/css-layout-debugger/)._

I've recently come across Gajus Kuizinas' [Favorite CSS hack][fav-css-hack]. I had used basic CSS debug styles (also known as [diagnostic CSS][eric-meyer]) before, and kind of love their simplicity. I wondered if I could take them up a notch.

## The Challenge

> __Create a CSS-only element inspector__: as you hover elements, show their bounding box, and how their descendants are laid out — all with minimal disruption to the layout.

## The Approach

You can see what I came up with [on this demo page][demo-page], and follow along as I go into the thought process, the dead-ends, and interesting tidbits of CSS I learned in the process. 

### Basic styling

To show an element's box, we can `outline` it:

```css
* {
	outline: 1px solid red;
}
```

The `outline` property does not alter the layout since it's painted on top of elements, and authors usually only include outline styles for focused elements, so it's relatively innocuous.

To discern how the elements are nested, let's also add [onionskin][onionskin] backgrounds:

```css
* {
	outline: 1px solid red;
	background: rgba(255, 0, 0, 0.1);
}
```

So far so good! This is starting to look promising, but everything is quite red.

### Cycling the hue

To further distinguish elements at different levels of nesting, let's vary the outline / background color (as in [Gajus' example]([fav-css-hack])). The `hsl()` notation, which is one of the few ways to obtain color variations in CSS right now, is a good candidate for expressing our colors.

First, a bit of refactoring, to bring `red` and `rgba(255, 0, 0, 0.1)` together:

```css
* {
	--hue: 0; /* red */
	outline: 1px solid hsl(var(--hue), 100%, 50%);
	background: hsl(var(--hue), 100%, 50%, 0.1);
}
```

With the `--hue` custom property in place, and the outline and background based on it, we can simply assign various values to it and have everything change harmoniously.

> __Note:__ HSL is good, but not _great_. It replaces the machine-oriented red, green, and blue channels with something humans can better relate to — hue, saturation, and lightness. It's not, however, very true to how we perceive colors. Maintaining a constant saturation and lightness, colors of various hues will look wildly brighter or darker to the human eye. When it gets implemented in browsers, [the `lch()` color notation][css-colors-4] will offer a better approximation. 

How do we go about cycling the hue in, let's say, 60 degree increments? You may be thinking, as I did, that CSS custom properties work like normal variables in other programming languages, and reach for:

```css
* {
	--hue: calc(var(--hue, 0) + 60);
}
```

However, CSS disallows cyclic dependencies between custom properties — sets of properties that define themselves in terms of one another — and that includes a property that refers to itself. In a future version of the spec we might [get a way][parent-var] to let an element redefine a custom property based on the value it has inherited from its ancestors, but until then, no dice. 

Leafing through the [CSS values and units spec][css-values-4], I was surprised to find [the `toggle()` function][toggle]. It enables elements to cycle over a set of values instead of inheriting the value. You'd be able to write:

```css
* {
	--hue: toggle(0, 60, 120, 180, 240, 300);
}
```

...to get elements at each subsequent level to move 60 degrees away from their parent. Nice and clear and... unsupported. Even though the `toggle()` idea has been floating around [since as early as 1999][toggle-origins], no browsers implement it at the time of writing.

So, unless I'm missing a clever workaround, we're back to old-school:

```css
* { --hue: 0; }
* > * { --hue: 60; }
* > * > * { --hue: 120; }
* > * > * > * { --hue: 180; }
* > * > * > * > * { --hue: 240; }
* > * > * > * > * > * { --hue: 300; }
```

This setup caps out at the 300 hue (a nice fuchsia), but you can continue to cycle it for as many `* > *` selectors as your heart lets you. 

### Showing the padding around elements

If the browser barely broke a sweat from anything we did so far, it's time to raise the temperature with our next mini-challenge: showing padding visually.

We're going to need something that lets us hook into the element's content box, and its padding box, independently. Hello [`background-origin`](https://developer.mozilla.org/en-US/docs/Web/CSS/background-origin)!

The `background-origin` property is only meant for background _images_, not background colors, so we need a way to make a background image out of a solid color. The [CSS Images Level 4][css-images-4] spec defines [the `image()` syntax][image] which accepts a color, such as `image(fuchsia)`, to produce an image. But until browsers support it, we need to improvise with gradients. 

The shortest formula to get a solid color with the gradient syntax is, as far as I know:

```css
* {
	background-image: linear-gradient(fuchsia, fuchsia);
}
```

To get different colors for the content box and the padding box, we layer two of these images and define their `background-origin` separately:

```css
* {
	background-image: 
		linear-gradient(fuchsia, fuchsia),
		linear-gradient(yellow, yellow);
	background-origin:
		content-box,
		padding-box;
	background-repeat: no-repeat;
} 
```

The order of multiple backgrounds is from closest to the user (topmost) to furthest. In the code above, the content-bound fuchsia sits on top of the padding-bound yellow. I never remember this order and have to look it up constantly. It's important to include `background-repeat: no-repeat` for it to work as expected, which I also tend to forget.

> __Note:__ You can probably get a similar effect with [`background-clip`][bg-clip] instead of the `background-origin` / `background-repeat` combo, but I haven't looked into it.

For a cooler look, like the one you sometimes see in dev tools, let's turn our yellow padding box into nice diagonal stripes. The [`repeating-linear-gradient`][repeating-gradient] is useful for this:

```css

{ 
	background-image: repeating-linear-gradient(
		45deg, /* angle for diagonals */
		fuchsia, fuchsia 1px, /* our 1px stripes */
		transparent 1px, transparent 3px /* 2px space between stripes */
	)
}
```

If we work it into our inspector code:

```css
* {

	/* Opaque version of the color */
	--c-solid: hsl(var(--hue), 100%, 50%);

	/* Translucent version of the color */
	--c-bg: hsl(var(--hue), 100%, 50%, 0.1);

	outline: 1px solid var(--c-solid);

	background-image: 
		
		/* Content box fill */
		linear-gradient(var(--c-bg), var(--c-bg)),

		/* Content box white underpaint */
		linear-gradient(white, white),

		/* Padding box stripes */
		repeating-linear-gradient(
			45deg,
			var(--c-solid), var(--c-solid) 1px,
			var(--c-bg) 1px, var(--c-bg) 3px
		);

	background-origin:
		content-box,
		content-box,
		padding-box;

	background-repeat: no-repeat;
}
```

To make the combo work with stripes, you may notice we've added a coat of white paint over the stripes along the context box, to obscure them.

### Making our inspector hover-aware

So far we've done a decent job of highlighting everything on the page. It would be easier to follow if we could localize the highlights to the element we're hovering.

Small problem: when you hover an element on the page, you don't just hover that element. You also hover all its ancestry. And the way CSS is designed does not allow us to select just the innermost hovered element. (We would need to know when a particular element contains another hovered element.) 

The closest we can get to the ideal is to change `*` (every element on the page) to `body :hover, body :hover > *`, which means _hovered elements and their direct descendants_. We're also not styling `body` itself, since it doesn't provide too much information and its add visual noise.

```css
body :hover, body :hover > * {

	/* Opaque version */
	--c-solid: hsl(var(--hue), 100%, 50%);

	/* Translucent version */
	--c-bg: hsl(var(--hue), 100%, 50%, 0.1);

	outline: 1px solid var(--c-solid);

	background-image: 
		
		/* Content box fill */
		linear-gradient(var(--c-bg), var(--c-bg)),

		/* Content box white underpaint */
		linear-gradient(white, white),

		/* Padding box stripes */
		repeating-linear-gradient(
			45deg,
			var(--c-solid), var(--c-solid) 1px,
			var(--c-bg) 1px, var(--c-bg) 3px
		);

	background-origin:
		content-box,
		content-box,
		padding-box;
	background-repeat: no-repeat;
}
```

__A note on `:focus`.__ Unlike `:hover`, only one element at a time has `:focus`, and we could model our approach around that. However, to make all elements focusable, and avoid triggering their default behavior when we click on them, we would need the help of some JavaScript:

```js
Array.from(document.querySelectorAll('*')).forEach(
	el => {
		el.setAttribute('tabindex', 0);
		el.addEventListener('click', e => {
			e.preventDefault();
			e.stopPropagation();
		});
	}
)
```

...which takes away from the beauty of a CSS-only solution.

### Extra credits: identifying the elements

One last mini-challenge: could we show information about elements — their tag name, ID and classes – as we hover them? 

CSS has [the `attr()` function][attr] to read attributes from HTML elements and use them in styles. At the time of writing, we can only use `attr()` as the `content` of `::before`and `::after` pseudo-elements, but for our modest goals it will do nicely. We can extract an element's `class` and `id` — but not its tag name — and display them as a floating label:

```css
[id]:hover::before,
[class]:hover::before {
	position: absolute;
	transform: translate(0, -100%);
	background: #000;
	color: hsl(var(--hue), 100%, 80%);
}

:not([id])[class]:hover::before {
	content: '.' attr(class);
}

:not([class])[id]:hover::before {
	content: '#' attr(id);
}

[id][class]:hover::before {
	content: '#' attr(id) '.' attr(class);
}
```

Let's unpack that.

The `[id]:hover::before, [class]:hover::before` selector matches the `::before` pseudo-element of hovered elements which have either an ID or a class attribute attached to them. 

Because we can't set these elements' `content` conditionally (based on which of the ID / class attributes are present) we set it for elements which have both (`[id][class]`), and separately for ones which have just one (`:not([id])[class]`) or the other (`:not([class])[id]`).

`position: absolute` takes the `::before` pseudo-element out of the normal flow. By not specifying `top` and `left` offsets we leave it in its default place at the top-left hand corner of its parent's content box. Instead, we use `transform(0, -100%)` to pull it upwards, so it sits on top of its parent. 

__Note:__ Since we're taking over the `::before` element, we might break aspects of the layout that may depend on it. And even so, the technique is not bulletproof, and might benefit from setting `position: relative` on the parent and explicit `top` and `left` offsets, with the risk of further altering the original layout.

### All together now

Here is the final version of the code:

```css
/* 
	Hue rotation 
	------------
*/

* { --hue: 0; }
* > * { --hue: 60; }
* > * > * { --hue: 120; }
* > * > * > * { --hue: 180; }
* > * > * > * > * { --hue: 240; }
* > * > * > * > * > * { --hue: 300; }
* > * > * > * > * > * > *{ --hue: 0; }
* > * > * > * > * > * > * > * { --hue: 60; }
* > * > * > * > * > * > * > * > * { --hue: 120; }
* > * > * > * > * > * > * > * > * > * { --hue: 180; }
* > * > * > * > * > * > * > * > * > * > * { --hue: 240; }
* > * > * > * > * > * > * > * > * > * > * > * { --hue: 300; }

/* 
	Draw elements' boxes
	--------------------
*/

body :hover, body :hover > * {

	/* Opaque version */
	--c-solid: hsl(var(--hue), 100%, 50%);

	/* Translucent version */
	--c-bg: hsl(var(--hue), 100%, 50%, 0.1);

	outline: 1px solid var(--c-solid);

	background-image: 
		
		/* Content box fill */
		linear-gradient(var(--c-bg), var(--c-bg)),

		/* Content box white underpaint */
		linear-gradient(white, white),

		/* Padding box stripes */
		repeating-linear-gradient(
			45deg,
			var(--c-solid), var(--c-solid) 1px,
			var(--c-bg) 1px, var(--c-bg) 3px
		);

	background-origin:
		content-box,
		content-box,
		padding-box;
	background-repeat: no-repeat;
}

/* 
	Show elements' classes / ID
	---------------------------
*/

[id]:hover::before,
[class]:hover::before {
	position: absolute;
	transform: translate(0, -100%);
	background: #000;
	color: hsl(var(--hue), 100%, 80%);
}

:not([id])[class]:hover::before {
	content: '.' attr(class);
}

:not([class])[id]:hover::before {
	content: '#' attr(id);
}

[id][class]:hover::before {
	content: '#' attr(id) '.' attr(class);
}
```

> To make it more resilient, we can sprinkle some `!important` keywords, but that's left as an exercise to the reader.

Here's [the demo page again][demo-page]. That is all! ✌️

[fav-css-hack]: https://dev.to/gajus/my-favorite-css-hack-32g3
[eric-meyer]: https://meyerweb.com/eric/thoughts/2007/09/07/diagnostic-styling/
[demo-page]: https://danburzo.ro/demos/css-only-layout-debugger.html
[onionskin]: https://en.wikipedia.org/wiki/Onionskin
[css-colors-4]: https://www.w3.org/TR/css-color-4/#lab-colors
[css-values-4]: https://www.w3.org/TR/css-values-4/
[css-images-4]: https://drafts.csswg.org/css-images-4/
[image]: https://drafts.csswg.org/css-images-4/#image-notation
[toggle]: https://www.w3.org/TR/css-values-4/#toggle-notation
[toggle-origins]: http://lists.w3.org/Archives/Public/www-style/1999May/0067
[attr]: https://www.w3.org/TR/css-values-4/#attr-notation 
[parent-var]: https://github.com/w3c/csswg-drafts/issues/1594#issuecomment-382832667
[bg-origin]: https://developer.mozilla.org/en-US/docs/Web/CSS/background-origin
[bg-clip]: https://developer.mozilla.org/en-US/docs/Web/CSS/background-clip
[repeating-gradient]: https://developer.mozilla.org/en-US/docs/Web/CSS/repeating-linear-gradient