Skip to main content

CSS guide: parents should tell children where to sit

The principle that reshaped how I wrote CSS.

When you write front-end code guided by developer comps, it’s easy to get into a reproduction mindset. “OK, here’s this button, it’s red and has rounded corners.” (Writes button style.) “Here’s another button, it’s green, it’s 18 pixels from the right of the red button.” (Writes another button style.) “Oh, here’s a close button, it’s at the top right of this div.” (Write close button style.)

This approach can produce so-called “pixel-perfect” designs, but it leads to code that’s a bear to change and difficult to reuse. Frequently, the offending code has to do with positioning.

button[type="submit"] {
	display: block;
	position: absolute;
	bottom: 0.5rem;
	right: 0.5rem;
	background-color: green;
	color: white;
}

When you let an element position itself, you’re making an implicit dependency on document structure. The submit button above only works as a style if every submit button needs to be placed at the bottom right of a container. Not only that, it needs specific style requirements on the container as well.

In a large codebase with people moving fast, this creates some surprising behavior and people will be tempted to add to the selector in new instances, then override the styles.

.squeeze button[type="submit"] {
	right: auto;
	left: 0.5rem;
}

Which isn’t the worst thing in the world, but then


.search button[type="submit"] {
	position: static;
	display: inline;
	margin-inline: 0.5rem;
}

Ah, we’re trying to re-align this instance to some other element inline. So we’ve overridden the position back to the default (static) and made it push other things off to the right and left sides. If we extend this search button further into another context, like so


.takeover .search button[type="submit"] {
	position: absolute;
	top: 0.5rem;
	left: 0.5rem;
}


what do you think will happen?

The old bottom and right positions previously ignored because the position was static are now re-activated, so we get a button that fills most of the space — set 0.5rem from the top and bottom but 1rem from the left and right.

1rem from the left and right? We said “.5rem.” Where’s that extra 0.5rem coming from?

.takeover .search button[type="submit"] {
	position: absolute;
	top: 0.5rem;
	left: 0.5rem;
	bottom: auto;
	right: auto;
	/* mystery additional 0.5rem from someplace, fix it */
	margin-inline: -0.5rem;
}

This seems like a silly thing to do, but these magic-number overrides are not uncommon at all in large, messy codebases.

Presumably what we want is a green button with white text, but where it sits depends on the container. We need to separate how the button looks from where it is on the page, and my favorite way to do that is to move the positioning rules to the parent.

In other words: parents tell children where to sit.

button[type="submit"] {
	background-color: green;
	color: white;
}

.contactForm {
	position: relative;

	> button[type="submit"] {
		position: absolute;
		bottom: 0.5rem;
		right: 0.5rem;
	}
}

.squeeze contactForm {
	position: relative;

	> button[type="submit"] {
		position: absolute;
		bottom: 0.5rem;
		left: 0.5rem;
	}
}

.search > button[type="submit"] {
	margin-inline: 0.5rem;
}

.takeover {
	position: fixed;
	inset: 0;

	> button[type="submit"] {
		position: absolute;
		top: 0.5rem;
		left: 0.5rem;
	}
}

None of the position elements need to be overridden in the new cases because they’re precisely targeted based on context. It leads to a little bit of repetition, but the results are a lot easier to reason about.

Notice in the above example that we’re placing these position rules using descendant selectors. This makes the implicit relationship explicit, which is exactly how you’d want to assign, for example, grid placement.

.pageGrid {
	display: grid;
	/* grid layout .... */

	> header {
		grid-area: header;
	}

	> footer {
		grid-area: footer;
	}

	> main {
		grid-area: main;
	}
}

This doesn’t solve all of the complexities a messy cascade presents, but it does fix many of them. When I refactor code, I always target the positioning logic first — and I try to follow it even on items (like page footers or the main content area) that are presumably not reused because:

  1. They might be placed differently on different page layouts
  2. It makes them a lot easier to include in pattern libraries

If you’re not using this strategy, give it a try for a bit and see if it doesn’t make CSS a lot more logical.

Add a comment
Endmark: No Silver Bullet