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:
- They might be placed differently on different page layouts
- 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