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:
-
ulfor any kind of list, like site’s sections inheader > nav. -
detailsfor table-of-contents (check the source of MDN). -
dl/dtfor list of pairs.
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:
-
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.
-
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:
- Zoom, which has a dedicated UI element, shortcuts/gestures, per-page persistence/overrides and a global default.
- Changing default font-size, a global setting buried deeply in the configuration page.
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!