DevelopmentJunior

HTML & CSS

HTML and CSS are the two languages every web page is built from: HTML says what each part of the page is, and CSS says how it looks and where it sits. Both are usually treated as the easy layer, and both run on rules precise enough that almost every “why does it look like that” has an exact answer. The page opens with twelve symptoms of a page that does not do what its code says, each with the rule behind it and the fix, then covers what the browser does with the two files, the promises HTML makes, the cascade, the box and the layout, what costs the user, and five documented cases.

Specified by
WHATWG · W3C
Layout tools
Flow · Flex · Grid
Home pages failing WCAG
95.9% · WebAIM 2026

Twelve symptoms and the rule behind each

Here is the table first, because it is the reason most people open a page about HTML and CSS: something on the screen is not doing what the code says. Each row is a symptom you can see, the rule that produces it, and the fix. None of the twelve is a browser bug. Each one is a rule that was always there, meeting code written by someone who had not met it yet — and there are few enough rules that the rest of this page covers all of them.

SymptomWhat is actually happeningThe fix
1. My style is ignoredIt lost at one of the cascade’s four gates, or the selector never matched the element.Find the gate you lost at (the cascade, below) before raising specificity.
2. z-index: 9999 does nothingThe element sits inside a stacking context that is itself painted lower; the number only competes inside it.Find the ancestor that creates the context — often a transform or an opacity — and fix the order there.
3. width: 100% plus padding overflowsBy default the width covers the content only; padding and border are added on top of it.box-sizing: border-box on everything — almost every CSS reset does it.
4. The gap between two blocks is smaller than both margins added upVertical margins between blocks in normal flow collapse into the larger one. It is in the spec.Space children with gap in a flex or grid parent, or with padding; neither collapses.
5. A long word or link bursts out of its cardA flex item will not shrink below its longest unbreakable word: its minimum width defaults to its content.min-width: 0 on the item, overflow-wrap: anywhere on the text.
6. The page scrolls sidewaysSomething is wider than the screen: a fixed width, 100vw (which includes the scrollbar), or an unbroken string.Find it by outlining every element for a minute, then switch to max-width and relative units.
7. position: sticky does not stickAn ancestor has overflow set and has become the scroll box, the parent is no taller than the element, or there is no top.Remove the ancestor’s overflow, give the parent room to scroll past, set a top.
8. An absolutely positioned element flies to the corner of the pageIt is placed against the nearest positioned ancestor, and there is none, so it uses the page.position: relative on the element it should be pinned to.
9. The page jumps while it loadsContent arrived with no space reserved for it: an image without dimensions, a late font, a banner inserted above.width and height, or aspect-ratio, on media; reserved space for anything injected later.
10. A full-screen block is cut off at the bottom on a phoneOn mobile 100vh is the height with the browser bar hidden, so the block is taller than what is visible.100svh for a stable height, 100dvh to follow the bar as it moves.
11. An animation stuttersIt animates width, top or margin, which reruns layout and paint on every frame.Animate transform and opacity, which only the last step of the pipeline has to redo.
12. A button works with a mouse and nothing elseIt is a <div> with a click handler: no focus, no keyboard, nothing for a screen reader to announce.A <button>. The section on HTML below lists what it gives you for free.

What the browser does with two files

HTML and CSS look like documents, so they get read like documents — top to bottom, as a description of the page. The browser reads them as input to a program it reruns every time anything on the page changes. Knowing the steps of that program is what turns the table above from twelve facts to memorise into one mechanism to understand.

First comes parsing. The HTML becomes the DOM — a tree of objects, one per element, that scripts can read and change. The CSS becomes the CSSOM, the same rules turned into objects the browser can query. Then four steps run in order. Style walks the DOM and, for every element and every property, runs the cascade to pick one value. Layout takes those values and works out the size and position of every box. Paint fills in the pixels, layer by layer, in stacking order. Composite assembles the painted layers on the screen, usually on the .

The browser turns HTML into the DOM and CSS into the CSSOM, then runs four steps every time something changes: style decides which value each property gets, layout works out every box’s size and position, paint fills in the pixels in stacking order, and composite puts the painted layers on screen. The accessibility tree is built from the HTML alone, which is why no CSS changes what a screen reader hears. The bars show the cost of a change: the earlier the step it restarts from, the more work it causes on every frame of an animation.

The branch on the right of the diagram is the one most people never hear about. Alongside the DOM, the browser builds an accessibility tree, in which every node has a role, a name, a state and the actions it supports. Screen readers, voice control and the operating system’s accessibility layer read that tree, and it is built from the HTML alone. No CSS reaches it. A <div> styled to look exactly like a button is, in that tree, a piece of text.

StepWhat it decidesRows of the table it explains
StyleWhich value each property of each element gets1
LayoutThe size and position of every box3–10
PaintThe pixels, and what is drawn over what2
CompositeHow the painted layers reach the screen11, and its fix
Accessibility treeWhat assistive technology can see and do12

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 same button written twice. On screen they are identical, which is why the difference survives every review done with a mouse. Underneath, the button element makes five promises the browser keeps for free — keyboard focus, Enter and Space, a role a screen reader announces, form submission, and a working disabled state. The div makes none of them, and every one has to be rebuilt by hand in JavaScript.

The diagram is the whole argument in one frame. Semantic elements put the right role, name and state into the accessibility tree without a line of code; a <div> puts in nothing, and every missing piece has to be added back with attributes and JavaScript. Most of the time the choice is not between two ways of building a button. It is between using the one the browser already built and rebuilding it worse.

The button is the famous case, and it is one of several. Each element below does work that teams routinely rebuild by hand, usually because the native one was not styled the way the design wanted and restyling it looked harder than replacing it.

ElementWhat you get for freeWhat gets built instead
<button>Focus, Enter and Space, a role, form submission, a working disabledA <div> with a click handler
<a href>A real link: open in a new tab, copy the address, followed by crawlersA button or span that changes the page from script
<label for>A name for the input, and a bigger target to clickPlaceholder text
<dialog>Opened with showModal(): focus moved inside, Escape closes it, the rest of the page inertA <div> with a high z-index
<details>Opens and closes from the keyboard, with no JavaScript at allA hand-rolled accordion
<nav>, <main>Landmarks a screen reader can jump betweenDivs with class names

Two habits 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. One attribute belongs beside them: the <html> element needs lang, because it is how a screen reader picks the voice and the pronunciation — and 13.5% of the home pages of the million most popular websites still leave it out.

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 — row 1 of the table — is answered by knowing which comparison you are actually losing at.

When several declarations set the same property on the same element, the browser resolves them in a fixed order: origin and importance first, then cascade layers, then specificity, and only then source order. Each gate eliminates candidates, and the last one standing is applied. Specificity is the third gate, not the first — which is why raising it is usually a fix aimed at the wrong stage.
GateWhat is comparedWhere people lose
1. Origin and importanceAuthor, user and browser styles, with !important inverting the order between them.An !important in a vendor stylesheet you did not write.
2. Cascade layersWhich @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. SpecificityIds, 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 appearanceThe 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. Layers have worked in every major browser since early 2022, so the reason not to use them is gone.

Two things get mistaken for the cascade. The first is inheritance: a few properties — color, font-family and line-height among them — pass from parent to child when nothing sets them, while most — margin, border, width — never do. An element showing the wrong colour with no rule of its own is not losing the cascade; it has nothing in it, and it is inheriting. The second is the pair of selectors that change specificity on purpose. :where() wraps a selector and gives it zero specificity, which is how a library ships defaults you can override with a single class. :is() takes the specificity of its most specific argument, which is how a harmless-looking selector suddenly wins.

The box, then the layout

Every element the browser lays out is a rectangle made of four layers: the content, the padding around it, the border, and the margin outside the border. Two defaults in that model produce two rows of the table. First, width sets the width of the content alone, so padding and border are added on top — which is row 3, and why almost every stylesheet starts with box-sizing: border-box, which makes width mean the whole box. Second, vertical margins between blocks in normal flow collapse: 24 pixels below one paragraph and 16 above the next produce a gap of 24, not 40. That is row 4. It is deliberate — it keeps running text evenly spaced — and it happens only in normal flow, never between flex or grid items.

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.

One flex default is worth knowing by name, because it produces row 5. The minimum width of a flex item is its content, so the item refuses to shrink below its longest unbreakable word or address and pushes out of its container instead. min-width: 0 lets it shrink. Grid has the same trap in a different spelling, and minmax(0, 1fr) is the same fix.

Positioning and stacking

Positioning takes an element out of the ordinary layout, and every variant is defined against something. relative shifts the element from where it would have been and leaves its space behind. absolute places it against the nearest ancestor that is itself positioned — and when there is none, against the page, which is row 8. fixed places it against the . sticky behaves normally until its scroll container would carry it out of view, then holds it in place — and the scroll container is the nearest ancestor with overflow set, which is why a stray overflow: hidden three levels up is the usual cause of row 7.

Then there is what draws over what. z-index is not a global ranking. It orders elements within a stacking context, and a new context gets created quietly — by a positioned element with a z-index, but also by opacity below 1, any transform, a filter or isolation: isolate. Everything inside a context is painted together, as one unit, at its parent’s place in the order.

A z-index is compared only with its siblings inside the same stacking context. The menu’s 9999 beats everything else inside A — and nothing outside it, because A as a whole is compared with B, and A’s 1 loses to B’s 2. The number cannot escape the context it lives in. The fix is to change the order of A and B, or to stop A from creating a context at all; the usual culprit is a transform or an opacity added for an animation, not a z-index anyone chose.

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.
  • Nesting rules inside rules works in plain CSS, in every major browser since 2023 — one of the last reasons a small project needed a preprocessor at all.
  • 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.
  • aspect-ratio reserves a box’s shape before its content arrives, which retires the padding-percentage hack and prevents row 9 for every video embed.
  • The viewport units svh, lvh and dvh describe a phone screen with the browser bar shown, hidden, or as it is right now — the fix for row 10.
  • 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.
  • gap works 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.

The bars under the pipeline diagram are the other half of the story. Changing a property restarts the pipeline from the first step that property affects, and during an animation that restart happens sixty times a second or more:

You changeSteps that rerunCost per animation frame
width, height, top, margin, font-sizeStyle, layout, paint, compositeExpensive — every box around it may move
color, background, box-shadowStyle, paint, compositeModerate — nothing moves, pixels are redrawn
transform, opacityStyle, compositeCheap — the painted layer is moved or faded as it is

That is the whole explanation of row 11. transform and opacity can be handled by the compositor alone, as long as the element sits on its own layer — which browsers arrange for anything being animated — so an animation built on them stays smooth even while the main thread is busy with something else. The same slide-in written with left runs layout for the element and everything around it on every frame.

Two visible failures are worth naming because both are cheap to prevent. Layout shift — row 9 — 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. Google measures it as CLS, one of the three it has used in ranking since 2021, and treats a score above 0.1 as needing improvement. Reserve the space and it does not happen. 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.

Five cases, and what each one was missing

Every case below is documented in public — by a court, a regulator or the company itself. In none of them was the missing piece advanced. It was an element, an attribute or a habit this page has already named.

CaseWhat happenedWhat was missing
Target, 2006–2008A class action by blind shoppers who could not use Target.com, settled with a $6M fundText alternatives for images; a checkout usable without a mouse
Domino’s, 2016–2019A blind customer could not order with a screen reader; the US Supreme Court declined to hear Domino’s appealControls a screen reader can name and operate
WebAIM Million, 202695.9% of a million home pages fail automated WCAG checks; 96% of the errors fall into six categoriesContrast, alt, labels, link and button text, lang
Facebook, 2020Over 400 KB of compressed CSS on the homepage, about a tenth of it used for the first screenA way to delete CSS safely: locality
Mobile 100vh, until 2022Full-screen layouts cut off under the browser bar on every phoneUnits for the real visible height: svh, dvh

Domino’s is the same failure a decade later, with the question of law settled. Guillermo Robles, who is blind, could not order a pizza through the website or the app with a screen reader, and sued in 2016. In January 2019 the Ninth Circuit ruled that the Americans with Disabilities Act (ADA) covers a business’s website and app when they are connected to its physical stores, and in October 2019 the US Supreme Court declined to hear Domino’s appeal. Since then an inaccessible checkout in the United States has been a legal exposure, not a backlog item. The WebAIM Million shows how far practice is from that: its February 2026 scan of a million home pages found failures on 95.9% of them, and 96% of all the errors fell into six categories — low-contrast text, missing alt, unlabelled form fields, empty links, empty buttons and a missing lang. Every one of the six is a default this page has already described.

You do not need their tooling to buy the same property. 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.

The last row is a different kind of case: a platform decision rather than a team’s mistake. Mobile browsers defined 100vh as the height with their address bar hidden, so for years every full-screen hero was taller than the screen it loaded on, and the workarounds were scripts that measured the window. CSS fixed it at the source with three units — svh, lvh, dvh — which reached Safari in March 2022 and Chrome in November 2022. If a codebase still sets a height from window.innerHeight, it is carrying a workaround for a problem that no longer exists.

Where it degrades

HTML and CSS rarely fail loudly. A page with a dozen of the problems above still renders, still passes review on a designer’s laptop, and still looks finished. These are the shapes the decay takes.

  • Divs with click handlers instead of buttons, which are invisible to the keyboard and to every assistive technology.
  • outline: none on 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 !important arms race, and its twin, the z-index arms race — each side can only reply with a bigger version of the same move.
  • 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.
  • An accessibility overlay — one script that promises to fix the page — in place of fixing the markup. In 2025 the US Federal Trade Commission made accessiBe pay $1 million for claiming its widget could make any website WCAG-compliant.
  • A perfect automated accessibility score treated as the finish line rather than the first check.

There is also an honest limit, and it matters more than any row in the table. Everything on this page makes the browser do what you meant. None of it tells you whether what you meant can be used. Automated checkers see part of that at best: when the UK Government Digital Service built a test page with 143 deliberate barriers in 2017, the best of ten tools found 41% of them, all ten together found 71%, and 29% were found by none — links marked only by colour among them, which pass every rule a machine can check.

The response is cheap and has no substitute. Put the mouse away and use the page with the keyboard for five minutes. Turn on the screen reader your operating system already has — VoiceOver on a Mac or an iPhone, Narrator on Windows, TalkBack on Android — and try to complete the one task the page exists for. Load it on a real mid-range phone over a slow connection. Ten minutes of that finds what no linter will, and it is the only check on this page that tests the promise rather than the code.

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.
  • An accessibility overlay in place of fixing the markup — it cannot repair what the HTML never promised.
  • Optimising selector performance, which has not been a real bottleneck for over a decade.

Sources and method

What this page stands on

  • 13 sources
  • 3 documented failures
  • Checked October 13, 2026

Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.

Found this useful?

Share it with someone who is working on the same problem.