css and layoutsAI

AI Writes CSS Like It's 2015 – and How to Change That

The dropdown menu in the header hides under the hero section. You select the component file, type "the menu hides under the hero, fix it" to the assistant, and a few seconds later you get:

css

.menu {
position: absolute;
z-index: 9999 !important;
}

It doesn't work. You type "it still hides" – and you get z-index: 99999, and in the next round position: fixed with a top computed in JavaScript. The same week you ask it to fix a product card that breaks in a narrow sidebar, and the model adds a third @media (max-width: 768px) overriding the width. The FAQ accordion? max-height: 1000px. A full-screen hero? height: 100vh. Horizontal scroll on mobile? body { overflow-x: hidden }.

Each of these answers is "correct" in the sense that someone once wrote it that way and it worked. But none of them addresses the cause, and some of them – as you'll see in a moment – break something else in a place the model never looked at.

Why the model reaches for old patterns

A language model doesn't choose the best solution. It chooses the most likely one – the one that most often appeared in a similar context in the data it learned from. And for CSS, that data is heavily skewed toward the past. Stack Overflow answers, tutorials, code from repositories, articles from 2010–2020 – an enormous mass of code written before cascade layers, container queries, :has() or svh units existed. For every good answer using @layer there are hundreds of older answers using !important.

Then there's timing. Features that are stable in every browser today often only landed in the last two or three years. The model may know them, but it has far fewer examples of them, and even fewer examples of correct use in a real project. It knows the syntax, but not the habit.

The third reason is the most important, because it won't go away with newer models: the model can't see the page. It gets the menu component file, and a layering problem almost never lives in the menu. It lives in an ancestor that creates a new stacking context – via position: sticky, transform or opacity – and that ancestor is in another file, another component, sometimes in a library's CSS. A model that sees only .menu has a single lever available: the number in z-index. So it raises it. It does exactly what a human without devtools would do.

Patterns worth recognizing right away

A few answers come back so often that it's worth treating them as a signal that "the model fixed the symptom, not the cause":

  • !important, or a z-index with several nines. Almost always means a specificity conflict or a stacking context somewhere higher in the tree. Specificity is solved by @layer – I wrote about it in the cascade layers article – and layering on the Z axis is solved by finding the ancestor that creates the context.
  • A media query for a component. A card that should change depending on where it's placed, not on screen width, needs container queries – I explained this in the container queries article, using exactly that kind of card in a sidebar.
  • max-height: 1000px in an accordion, and JavaScript waiting for transitionend before hiding an element. Today you can animate height: auto and display: none in CSS alone.
  • height: 100vh on a full-screen section. On a phone it's taller than the visible screen, because vh is measured with the browser toolbars hidden.
  • overflow-x: hidden on body. It hides the horizontal scroll, but quietly turns overflow-y into auto and disables every position: sticky on the page.
  • An IntersectionObserver, a scroll listener or a library for effects CSS can already do on its own: scroll-linked animations (covered in the scroll-driven animations article), detecting a stuck header, masonry layout.
  • Four copies of the color palette for dark mode – in :root, in @media (prefers-color-scheme) and in two [data-theme] selectors.

I'll cover each of these points in its own post over the coming weeks. What matters more here is that they all share a common denominator. The model picks a solution that fits inside the file you showed it, and that was the most common one a few years ago.

A rules file: give the model the context it lacks

The first way to change these habits is simple and pays off surprisingly well: a project rules file that the assistant loads for every task. In Claude Code it's CLAUDE.md, in Cursor it's files in .cursor/rules, in GitHub Copilot it's .github/copilot-instructions.md, and more and more tools also read a shared AGENTS.md. The name differs, the mechanism is the same: text that enters the model's context before it sees your request.

It works because it shifts the probability distribution. Without rules, the most likely answer to "the menu hides under the hero" is z-index: 9999. With the rule "don't raise z-index until you've identified the ancestor creating the stacking context", the most likely answer becomes looking for the ancestor. The model doesn't need to "know more"; it just needs to know what you expect.

Here's a CSS section worth starting with:

markdown

## CSS
Target: browsers from the last 2 years (Baseline). Don't add fallbacks
for older ones without asking.
Architecture
- Layer order: @layer reset, base, components, utilities.
Every rule belongs to a layer.
- No !important outside the utilities layer.
- Scope component styles with @scope, not long selectors.
Layout
- Components respond to their container (container queries), not the
viewport. Media queries only for page-level layout.
- No fixed height on elements with content. Use min-height, grid, subgrid.
- Screen height: svh (default) or dvh. Never 100vh. Never 100vw for
full width.
- Clipping content: overflow: clip, not hidden. Never overflow-x: hidden
on body.
Layers
- z-index only from --z-* variables. Before changing z-index, identify
the ancestor that creates the stacking context and explain why.
- Component root with layers inside: isolation: isolate.
- Modals, menus and tooltips: <dialog> or popover (top layer), not z-index.
Colors and theme
- Only tokens from tokens.css. Don't introduce new hex values.
- Dark mode: light-dark() + color-scheme. No duplicated palette.
Animations
- Animate transform and opacity. Enter/exit: @starting-style,
transition-behavior: allow-discrete. Always prefers-reduced-motion.
Before fixing a layout, layering or scrolling bug: state the cause
(which element, which property) first, then the fix.

A few things in this file are deliberate. The rules are specific – "never overflow-x: hidden on body", not "write modern CSS", because the model will interpret a vague slogan its own way. The bans come with replacements – a ban on !important alone will make the model raise the selector's specificity instead; a ban plus @layer gives it the right tool. The last paragraph enforces an order of thinking: cause first, then code. That one sentence changes the answers more than all the others, because it forces the model to state what it doesn't know – and then you can see when it's working on a guess.

Adapt the file to your project. If you support older browsers, say which ones. If you use Tailwind, the rules about layers look different. The file should describe your project, not an ideal one.

The pitfall: the model fixes the symptom because it can't see the page

A rules file changes habits, but it doesn't solve the third problem from the beginning: the model still sees code, not the page. The rule "identify the ancestor creating the stacking context" won't do much if that ancestor isn't in any file the model has read. Then you get polite guessing: "one of the parents probably has a transform".

The missing piece is what you see in devtools: computed styles and the tree of ancestors. The simplest way to hand them over is a short console script and pasting the result into the conversation:

javascript

// Select the problematic element in the Elements tab (in the console: $0)
let el = $0
const out = []
while ((el = el.parentElement)) {
const s = getComputedStyle(el)
out.push({
el: el.tagName.toLowerCase() + (el.className ? "." + el.className.split(" ").join(".") : ""),
position: s.position,
zIndex: s.zIndex,
overflow: s.overflowX + " / " + s.overflowY,
transform: s.transform !== "none",
opacity: s.opacity,
})
}
copy(JSON.stringify(out, null, 2)) // the result lands in your clipboard

You paste it along with your request, and suddenly the model has the same data an experienced frontend developer would have: it sees that three levels up there's a header with position: sticky and no z-index, and it suggests a z-index on the header instead of the menu. The answer stops being a guess.

The second, more convenient way is to give the assistant direct access to the browser. Agents like Claude Code can drive a browser through an extension or an MCP server – Playwright, Chrome DevTools – and read computed styles themselves, walk the DOM tree, and take a screenshot before and after the fix. That changes the nature of the work: instead of "write a fix" you can ask "find out why the menu hides under the hero, show me the cause, then fix it and take a screenshot". A model that can check the result stops handing over code that "should work".

Ask for the cause, not the code

The simplest change requires no file and no tool – just a different request. Compare:

markdown

Bad:
The menu hides under the hero. Fix it.
Good:
The menu (.site-nav__dropdown) hides under the .hero section.
Before changing anything: which element creates the stacking context
the menu lives in, and why does the hero cover it?
Don't raise the z-index of the menu itself.
Below are the computed styles of the menu's ancestors:
[pasted JSON]

The first request leaves the model only one path – the shortest, which is the most likely one. The second closes that path explicitly, provides data and asks for an explanation. Good code review practice works here just as it does with code from a junior developer: if the author of a fix can't say why it works, it probably doesn't work for the reason they think – or it works by accident and will stop at the next change.

The second pitfall: new syntax that doesn't exist

Once you've convinced the model to write modern CSS, the opposite problem shows up. Features that are only now landing in browsers change syntax during standardization – and the model can confidently write a version that was in a spec draft two years ago and never shipped in any browser. Or correct syntax for a feature that works in only one engine today, without a word about a fallback.

Code like that doesn't produce errors: an unknown CSS property is silently ignored. So for every feature you don't know from your own experience, check it on MDN or caniuse before it lands in the repository – and add to your rules file which new features are allowed in the project and which require @supports.

Pitfalls and best practices

  • !important and z-index: 9999 from AI are a signal, not a solution. They almost always mean a specificity conflict or a stacking context in a place the model didn't see.
  • A rules file changes the model's habits. CLAUDE.md, .cursor/rules, copilot-instructions.md – specific rules with replacements work, vague slogans don't.
  • Enforce the order: cause first, then code. One sentence in the rules or the request changes more than a list of bans.
  • Give the model what you see in devtools. Computed styles of ancestors pasted into the conversation, or browser access via MCP, turn guessing into diagnosis.
  • Check new features at the source. The model may know outdated syntax or skip over missing support. MDN and caniuse before committing.
  • Review AI code like code from a new team member. If a fix comes without an explanation, ask for one before accepting it.

Summary

An AI assistant writes decade-old CSS for three reasons: it learned mostly from older code, it knows new features from far fewer examples, and above all it can't see the page – only the file you showed it, so it fixes the symptom with the only lever it has at hand. A rules file shifts its habits toward modern CSS, a "cause first" request forces diagnosis instead of guessing, and computed styles or browser access give it what it lacks most: a picture of the real page. None of these tools excuse you from understanding CSS – on the contrary, the better you know why z-index: 9999 doesn't help, the faster you'll recognize when the model is trying to sell it to you. In the coming posts I'll go through the problems listed above one by one.