CSS: Unavoidable Bad Parts

An ersatz CSS tutorial for people who need to style a web page, but aren’t web developers. I am a wrong person to write this kind of thing, as I have neither the time, nor experience. I’d much rather read a book about this. Alas, I had to learn all this stuff from trawling MDN, so perhaps it is valuable to document what I have so far.

CSS, HTML and Web APIs are truly vast, and it takes a career to become a professional. The good news is that modern web has a reasonably-sized, learnable subset which is enough for simple tasks like a programming blog or a simple GUI. I haven’t seen a resource that teaches just this subset, but it’s not too hard to figure this out. The bad news is that there’s also a nasty set of gotchas, which will mess up your page, which you won’t suspect to exist, and which will need days of debugging to figure out. Still, it’s not that bad. I am quite happy with the styling on this site, and it’s only about 200 of readable CSS.

Good: HTML5 semantic tag names
It’s worth looking through MDN Elements Reference. There aren’t that many elements, and things like main, article, nav, kbd make it much easier to structure your page. Less obvious:

Bad: Wrappers
If you “View Source” on any “real” website, you’ll notice that everything has layers and layers of wrapper elements, so you might be tricked into thinking that wrappers are how you solve layout problems. I can’t really agree or disagree here, as I never wrote “production” CSS, but, in my experience, it’s much easier to understand if you do the opposite — restrict yourself to using only markup-meaningful semantic tags, and then figure out CSS which works with the markup you have.

Bad: Layout
This one is not an exclusively Web problem, layout is a struggle in every GUI framework I know. Imagine a fixed sized raster image, and a paragraph of text describing it. There are many ways to arrange these two elements on the screen’s rectangle. Generally, for every given width and height, you can do a decent job, as long as the total area is enough. A typical GUI is a hierarchy of such boxes, with a lot of “layout freedom”. The problem though is that layout of each box affects the layouts of all other boxes, as you generally want all boxes to meet exactly, without gaps and overlaps. An important negative realization is that the layout algorithm doesn’t exist. There isn’t a fully general solution to positioning and sizing GUI boxes. Rather, different systems use different sets of heuristics to do the job, from simple RectCut, to fully general constraint solvers, with everything in between. It is hard to get the mental model of how layout works, in general. So, don’t think “how can I do my layout in a given system”, think instead “what possible layouts are allowed by the system”.

Bad: Browser defaults
Let’s start with a bare (but still semantic) HTML markup of a blog article, without any CSS. If you open it in a browser, it will show something. The content isn’t unstyled — the text is of a certain color, font and size. Headers are bigger than the main text, links are underlined, etc. These are the default styles of your browser. They are helpful! The problem is that these styles differ between the browsers. So, even when you add your own CSS, and the end result looks fine in your browser, I might see something different, because you might rely on a browser default, without knowing it. The last bit is the killer here — the problem is in something you didn’t write.

The general solution here is a CSS reset, or normalization — starting your CSS with an explicit set of rules, overriding defaults. Not because defaults are inherently bad, because they are inconsistent. I don’t know which set of rules you need to override in practice, it’s a good idea to compare several existing CSS resets.

This touches on the big question: should you style your web page? There are two competing views of the Web platform — some people treat it as a flexible, adaptive, primarily visual medium for expressing design, others would prefer if the Web focused on delivering the content, allowing each user to customize the presentation. My personal answer here is pragmatic — by default, an unstyled page is poorly usable and looks bad. I would have preferred the world where CSS-less pages were readable as is, but, in this world, I think it is helpful to style the content. At the same time, it’s a good idea to allow advanced users to bring their own CSS. Make sure that your HTML markup is reasonable, that you don’t overfit your HTML to CSS (vice-versa is fine), and that your page functions in reader mode.

Good: Classless CSS
You can’t reset styles to true neutral nothing: if you make the text invisible (white or transparent), it is still a style. So you might as well embrace it: after reset, style common HTML elements directly. For example, to set your favorite font for all code snippets:

code { font-family: "JetBrains Mono", monospace; }

If you use main, header, footer, nav tags you can set the overall page layout without writing any CSS selectors. This of course requires making assumptions, in CSS, about the structure of your HTML, but, like, this is your HTML and your CSS, you can do whatever, and, if you don’t like the result, you can always change it!

Bad: CSS selectors
In programming, we collectively came around to distrust inheritance and prefer composition. Default CSS is like supercharged inheritance, each design element on your web page is affected by multiple rules, and you can always “monkey patch” existing elements by appending to your CSS. There’s an unfortunate gap between CSS affordances, and what you actually want to do. The two reasonable approaches are:

  1. Conclude that CSS selectors add abstraction capability along the wrong axis, and stick to classless CSS and inline styles, using something like Tailwind to make writing inlines prettier, and something like JSX (or any other templating engine supporting composition) to avoid repetition in HTML.

  2. Use CSS nesting to avoid writing “far reaching” selectors and style component-per-component:

header { /* Site Header */
    margin-bottom: 2rem;
    & nav {
        /* Styles, specific to nav in the Header. */
    }
}

Bad: box-sizing
UIs are recursive rectangles, layout is the process of figuring out where each rectangles goes, and it is determined by the sizes of rectangles themselves. So, understanding what is the size is quite fundamental. Sadly, by default the definition of size in HTML is very unintuitive: element’s width and height do not include element’s border and padding, which leads to surprising results: everything looks perfect at first, but increasing padding somewhere shifts the entire layout unexpectedly. For this reason, * { box-sizing: border-box; } deserves to be the first line in your CSS reset. It makes elements encapsulated, such that adding borders is a local-only change.

Chaotic Good: margin collapsing
Suppose you want to have a 8px gap around an element. You would think that you need to set the padding property. But that would be wrong — if you have two such elements next to each other, the gap between them would be 16px. The paddings would add, creating a visual gap larger than intended. You want something more akin to social distancing, where if one person is more introverted, this person’s bigger radius of exclusion is what defines the distance. And that’s how the margin property works. Two neighboring margins are combined using max rather than sum. Margin collapsing is very useful, but it can surprise you. E.g. I think child margin can stick beyond parent’s? To be honest, I don’t have a good intuitive understanding of margins, but I know enough to at least identify when it is the problem.

Margins are also one of the indirect inspirations for this post. In

Moving away from Tailwind, and learning to structure my CSS

Julia Evans writes that you generally don’t want to set margin on an element, and should rather let the parent control the inter-element margin of the children, using the so-called owl selector:

section > *+* {
  margin-top: 1rem;
}

That is, add margin to all section’s children exempting the first one. I didn’t know that! And, given all the pain that margin gave me so far, I actually get why you want to do this, and why this is a good idea. But it bugs me that you can’t learn that without becoming “professional” web developer, or reverse-engineering someone else’s CSS framework.

Bad: Default (flow) layout
Layout in general is tricky, because there’s no universal “layout algorithm”, just a bunch of special cases. But what does HTML actually do? The default layout algorithm I think goes back to the origin of HTML as a language for documents, and overfits a use-case of producing papers — mostly text content with some illustrations, where the text can flow around the pictures. That’s actually what you want for the main body of text of your blog, but, as soon as you want to actually control the spatial arrangement of the elements on your page, you want something different, for example…

Good: flexbox
This is really what separates modern web-development from the olden days, where you’d need a CSS PhD or a full-blown opaque CSS framework to be able to say “this goes to the left, and this goes to the right”. This layout allows you to arrange a series of elements either vertically or horizontally, adapting to the available space. It is rather complex and I can’t use flexbox without referencing MDN all the time, but usually I am able to get things done in the end.

Bad: responsive design
Modern CSS allows querying screen size, and implementing conditional logic based on that — a design that “responds” to user-agent constraints. This probably what you should use for “real” CSS, but note that HTML is inherently responsive. Unlike PostScript (PDF), it will automatically reflow the paragraphs when you change window size. So, it’s a good idea to avoid writing explicit responsive rules, and just rely on layout to do the reasonable thing. For example, this blog looks OK on mobile, tablet and desktop without any explicit @media queries. Unconditionally setting max-width on the main column of text is all that it takes.

Lawful Evil: pixels
1px does what you want, but not what it says. It’s not a size of one physical pixel on your screen. Rather, it’s a measure of visual angle. That is, 1px should look perceptually the same on any screen, and it is converted to different number of physical pixels, depending on the screen size, its pixel density, and the typical viewing distance. So you can just size everything in pixels, without thinking about different displays’ pixel densities. It gets weirder. CSS allows “real” units like centimeters or inches, but they are also angles, because everything is defined in terms of pixels.

Doubleplusungood: font-size
Flexbox is a good way to layout UI-elements. Flow layout works ok for laying out paragraphs of text. But what happens on the level of individual lines and glyphs is, in my opinion, a train wreck and a noob trap. Let’s start with the basics: if you write font-size: 16px then 16px is the size of what? Sadly, the answer is “nothing in particular” — this is a size of a virtual box around the glyph, but the box isn’t tight, and the size of the glyph varies, depending on the font. Luckily, font-size-adjust property can fix it, and make font-size consistent across fonts. See these two posts for details:

Though, at the moment font-size-adjust seems to be very niche, so, while personally I’d put font-size-adjust: ex-height 0.53; right next to box-sizing, few pages do that.

The next issue with font-size is a thorny question of defaults. The good news is that it’s one of the properties that is fairly consistent across browsers, with 16px being the overwhelming default. The bad news is that, depending on the font, 16px can be on the smaller size. Not completely illegible, but very close to the lower bound. What’s worse, some default fonts are particularly small. For example, on Apple, font-family: serif looks much smaller than sans-serif, and is almost uncomfortable to read at 16px.

Can you just set font-size: 18px or whatever works best for your chosen font? I think the answer is yes, but there are some caveats to keep in mind. Refer to Accessibility: px or rem? for details. The issue is that modern browsers support two ways of making text on a page bigger:

Setting font-size in your CSS disables that second approach.

Taking everything together: don’t assume that text on your page will be readable by default, check different configurations. Set font-size-adjust to reduce the number of degrees of freedom and to pin down the meaning of font-size. If the result looks fine with your chosen (or your user’s default) font and default font-size of 16px, then you are done. Otherwise, set font-size to a bigger number. Afterwards, check that the page is readable in reader mode as well.

Bad: line-height
Despite the name, line-height doesn’t set the height of a line. It is a height of a run of glyphs, set in the same font. The two coincide when all the text is in the same font. But if you have, e.g., some words set in monospace font, you are in for a surprise. While font-size-adjust fixes the size of a glyph inside the box, it still leaves its relative position unspecified. So, when two runs of text in different fonts are aligned vertically to share the baseline, their line-height line-boxes get shifted relative to each other: one sticks below, one sticks above. The line height overall becomes larger that what you’d expect, as it is configured as a union. See

Deep dive CSS: font metrics, line-height and vertical-align

for a thorough explanation of this effect.

Bad: vertical rhythm
If you google long enough this cluster of problems, sooner or later you’ll come across the idea of vertical rhythm, that you should make sure that lines are in the same relative position across different paragraphs, even if you have headings, images, and what not. As if there’s invisible lined paper behind your web-page. As far as I can tell, this is pure voodoo and is not useful. If you do two-column layout, then you want lines on opposite sides to align, but it makes no sense to jump through hoops for a single-column layout (hat tip to @chrismorgan).

Bad: word-break
The genius of the flow layout is its dynamism. It takes a moment of reflection to appreciate the technical marvel of text breaking itself neatly into lines as the window is resized to be narrower. Getting that to work for the first time ever in the world of durably printed text must have felt incredible. But the magic has its limits — you can only break the line at the whitespace, or at the hyphenation points. And some long spans, like inline code or URLs, might be unbreakable. This leads to overflow annoyance on mobile devices, something you notice only after you publish your work. There’s no one trick to fix it, but some tips are available here: Against Horizontal Scroll for details.


And … that’s all I remember so far? I reiterate my request for someone to write a short 100-page book explaining just enough of HTML&CSS to make a simple blog without getting collapsed by the margins!