HTML & CSS
HTML and CSS are the two languages every web interface is made of, and the layer most often treated as the easy one. HTML is not markup for looks — each element is a promise the browser and assistive technology act on. CSS is not a list of styles — it is a resolution algorithm with a defined order, and knowing that order is what turns guessing into reasoning.
- Specified by
- WHATWG · W3C
- Cascade gates
- Four, in order
- Layout tools
- Flow · Flex · Grid
HTML is a set of promises
A <button> is not a box that looks like a button. It is focusable with the keyboard, it responds to Enter and Space, it announces itself to a screen reader as a button, it participates in form submission, and it is exposed to browser automation and to the operating system's accessibility layer. A <div> with a click handler has none of that, and reproducing all of it correctly takes about forty lines that you will get subtly wrong.
The mechanism behind that is the accessibility tree. As the browser parses your HTML it builds a second structure alongside the DOM, in which each node has a role, a name, a state and a set of supported actions. Screen readers, voice control, browser reading modes and search crawlers read that tree, not your CSS. Semantic elements populate it correctly for free; non-semantic ones leave it empty and require ARIA attributes to fill in by hand.
Two things carry most of the practical benefit. Headings must describe the structure of the document rather than its type size — many people navigate a page by jumping between headings, and a page whose only <h1> is the logo gives them nothing. And every form control needs an associated <label>; a placeholder is not a label, because it disappears exactly when the user needs it and is frequently not announced at all.
The cascade is an algorithm
When two declarations set the same property on the same element, the browser does not guess. It runs four comparisons in a fixed order and stops at the first one that separates them. Almost every "why is my style not applying" question is answered by knowing which comparison you are actually losing at.
| Gate | What is compared | Where people lose |
|---|---|---|
| 1. Origin and importance | Author, user and browser styles, with !important inverting the order between them. | An !important in a vendor stylesheet you did not write. |
| 2. Cascade layers | Which @layer a rule belongs to; layers are compared in the order they were declared. | A very specific selector inside an early layer still losing to a plain one in a later layer. |
| 3. Specificity | Ids, then classes and attributes and pseudo-classes, then elements — compared as three columns, not summed. | Assuming eleven classes beat one id. They do not — the id column is checked first. |
| 4. Order of appearance | The last matching declaration in source order wins. | Bundler output ordering your files differently from what you expected. |
Cascade layers are worth adopting for one reason above the rest: they let you separate "how strongly should this win" from "how precisely does this select". Put a third-party stylesheet in an early layer and your own overrides in a later one, and you can stop escalating selectors entirely — a single class in a later layer beats an id in an earlier one, because gate two is decided before gate three.
Layout: flow, flex, grid
Before any of the layout systems, there is normal flow — the behaviour a document has with no CSS at all. Block elements stack down the page and fill the available width; inline elements sit along a line and wrap. A great deal of layout code exists to recreate something normal flow already did, and the cheapest layout is the one you do not write.
Flexbox — one axis
- Content decides the sizes; the container distributes space
- A row or a column, wrapping if you allow it
- Right for toolbars, button rows, card internals
- Alignment along and across the one axis
Grid — two axes
- The container decides the structure; content fills it
- Rows and columns defined up front, by name if you like
- Right for page structure and any real two-dimensional layout
- Items can be placed and can overlap deliberately
The decision rule is short: if you are arranging things along a single line and the content should decide the sizes, use flex; if you are defining a structure that content drops into, use grid. They compose freely — a grid page with flex inside each cell is the ordinary case, and arguing about which is "better" is arguing about whether a hammer beats a saw.
What modern CSS removed
A large amount of received wisdom in this area is advice for problems that no longer exist. If you learned CSS from material written before roughly 2022, several of these will be new, and each one deletes a workaround rather than adding a technique.
- Custom properties (
--gap) are real values the browser resolves at runtime, so they cascade, respond to media queries and can be read and set from JavaScript — unlike preprocessor variables, which are gone by the time the browser sees the file. clamp()replaces most breakpoint-based type scales: one declaration gives a minimum, a fluid middle and a maximum.- Container queries let a component respond to the width of its own container rather than the viewport, which is what a reusable component actually needed all along.
:has()selects a parent by what it contains — the "parent selector" that was declared impossible for two decades.- Logical properties (
margin-inline,padding-block) describe direction relative to the writing mode, so a layout works in a right-to-left language without a mirrored stylesheet. gapworks in flexbox and grid alike, which removes the last reason to put margins on children and then strip the first or last one.
What actually costs the user
CSS is render-blocking by design: the browser will not paint until it has the stylesheets for the current media, because painting first and restyling second would flash unstyled content at everyone. That is why the size and the number of stylesheets in the document head is a first-paint problem rather than a bandwidth one.
Two visible failures are worth naming because both are cheap to prevent. Layout shift happens when content arrives without space reserved for it — an image with no dimensions, a font swapping to a different metric, an ad slot appearing above what someone is reading. Reserve the space and it does not happen. And layout thrash happens when JavaScript reads a geometric property, writes a style, then reads again in a loop, forcing the browser to recalculate layout synchronously on every iteration; batch the reads, then the writes.
How this shows up in real delivery
The thing that makes a stylesheet unmaintainable is almost never its size. It is that nobody can predict what deleting a rule will break, which happens when selectors reach across component boundaries — a rule in the header file that styles something in the footer because both matched .card h3. Whatever methodology you use to prevent that (a naming convention, CSS modules, scoped styles in a framework, or layers plus discipline), the property you are buying is the same one: locality, so a change stays where you made it.
Where it degrades
- Divs with click handlers instead of buttons, which are invisible to the keyboard and to every assistive technology.
outline: noneon focus with nothing put in its place, which makes keyboard navigation impossible to follow.- Headings chosen for their size, which destroys the document outline people navigate by.
- An
!importantarms race, where each side can only reply with another one. - Selectors reaching across component boundaries, so nobody can delete a rule safely.
- Magic numbers —
margin-top: 37px— which encode one screen size and break on the next. - Images without dimensions, which move the text under a reader's finger as the page loads.
When to use it
Use it when
- Always — every framework on this list compiles down to these two languages.
- Reach for plain HTML and CSS first for content pages, where a framework adds cost and no capability.
- Use native elements before ARIA, and ARIA only to describe what you have genuinely implemented.
- Use cascade layers when integrating third-party styles you cannot edit.
Avoid it when
- Hand-rolling widgets that already exist natively — a dialog, a details disclosure, a date input.
- Adding a CSS methodology to a project small enough that locality is not yet a problem.
- Reproducing a layout system in JavaScript because a resize listener felt easier than grid.
- Optimising selector performance, which has not been a real bottleneck for over a decade.
Found this useful?
Share it with someone who is working on the same problem.