DEV Community

Cover image for Do AI Coding Agents Need a Different CSS Architecture?
Marvin Ahlgrimm
Marvin Ahlgrimm

Posted on

Do AI Coding Agents Need a Different CSS Architecture?

Does BEM Still Make Sense When AI Writes CSS?

I use BEM. Sometimes.

Most of my CSS is plain CSS, with a main stylesheet and a few additional files for specific areas, such as blog.css. The HTML is rendered on the server using Marten. During deployment, a minifier takes care of the CSS, and Marten's collectassets handles fingerprinting.

That's pretty much my entire CSS toolchain. And I like it. There's very little overhead, and it does everything I currently need.

But with AI coding agents writing more of my code, I started wondering whether my approach still made sense. Should I use BEM more consistently? Switch to data attributes for modifiers? Or consider something like Tailwind or CSS Modules?

I ended up asking a Codex agent whether replacing BEM modifiers with data-* attributes would improve an existing project.

Its answer was rather simple:

data-* doesn't automatically make sense as a replacement for BEM modifiers.

Well, that wasn't quite the answer I expected.

But it got me thinking. Maybe the more important question isn't which CSS methodology an agent should use, but how to make sure it understands the decisions already made in a project.

BEM, modifiers, and data attributes

Here's a typical BEM example:

<article class="card card--featured">
  <h2 class="card__title">Hello World</h2>
  <p class="card__description">
    A short description.
  </p>
</article>
Enter fullscreen mode Exit fullscreen mode

I like how predictable this is. The relationship between the elements is obvious, and I don't have to worry too much about a generic .title selector affecting something elsewhere.

That's useful when working with global CSS.

Of course, BEM doesn't actually isolate styles. It relies on naming conventions to avoid collisions. And I don't think every simple layout needs a complete BEM structure.

But what about modifiers?

Instead of:

<button class="button button--primary button--large">
  Save
</button>
Enter fullscreen mode Exit fullscreen mode

I could write:

<button
  class="button"
  data-variant="primary"
  data-size="large"
>
  Save
</button>
Enter fullscreen mode Exit fullscreen mode

I actually prefer the second version here. The attributes make it clear that variant and size are independent properties.

Modern CSS handles this quite nicely:

.button {
  padding: 0.5rem 1rem;

  &:where([data-variant="primary"]) {
    background: var(--color-primary);
    color: white;
  }

  &:where([data-size="large"]) {
    padding: 0.75rem 1.5rem;
  }
}
Enter fullscreen mode Exit fullscreen mode

Using :where() prevents the attribute condition from adding specificity. Without it, .button[data-variant="primary"] would be more specific than .button--primary.

So far, so good.

But Codex had a point. Compare:

<div class="card card--flat">
Enter fullscreen mode Exit fullscreen mode

With:

<div class="card" data-variant="flat">
Enter fullscreen mode Exit fullscreen mode

What exactly do I gain by replacing the first with the second?

Not much, especially if the existing modifier is already understood throughout the project.

I still like data attributes for components with multiple named variants. But changing working code just because another syntax looks a little cleaner doesn't seem particularly useful.

Do we need the modifier at all?

The Codex agent also raised a more useful point: if a component already exposes its state through a semantic attribute, why introduce another representation just for styling?

Imagine an accordion trigger:

<button
  class="accordion__trigger accordion__trigger--open"
  data-state="open"
  aria-expanded="true"
>
  Details
</button>
Enter fullscreen mode Exit fullscreen mode

Now we have three representations of the same state. The BEM modifier says it's open, the data attribute says it's open, and aria-expanded says it's open.

Why?

For an ordinary disclosure component, I could avoid this entirely by using native HTML:

<details class="accordion">
  <summary class="accordion__trigger">Details</summary>
  <p>Some additional information.</p>
</details>
Enter fullscreen mode Exit fullscreen mode
.accordion:where([open]) > .accordion__trigger {
  font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

The browser already manages the open state. And if I need several disclosure elements where only one can be open at a time, the name attribute on <details> can handle that too.

Not every accordion or disclosure interface fits the native element, of course. For a custom implementation, aria-expanded may still be necessary. In that case, I can use it directly for styling:

.accordion__trigger:where([aria-expanded="true"]) {
  /* Expanded styles */
}
Enter fullscreen mode Exit fullscreen mode

Now there's less state to keep synchronized.

The same applies to disabled buttons. HTML already has disabled, and CSS provides :disabled. There's no reason to invent another representation of that state.

This doesn't mean adding ARIA attributes just for styling. They still need to describe the interface correctly. And data-* remains useful for custom states that don't have an appropriate native or ARIA representation.

But before introducing another modifier, it's worth checking whether HTML already provides what we need.

Would another CSS methodology help?

I also looked beyond BEM and data attributes.

CSS Modules, Tailwind, CUBE CSS, ITCSS, and other approaches offer different ways to organize or write styles. But for my setup, only a few seem particularly relevant.

CSS Modules

CSS Modules solves one of BEM's problems differently: instead of relying on globally unique class names, it scopes them during the build.

That's attractive. Classes like .title or .icon can be local to a component without requiring a naming convention.

But I'd also need a transformation step and a way to use the generated class mappings in my Marten templates.

If I were already using a component-based frontend with a build pipeline, CSS Modules would be an obvious candidate.

For my current setup, I don't have enough problems with class collisions to justify adding that integration.

Tailwind CSS

I initially associated Tailwind with a much larger frontend toolchain. That wasn't entirely fair.

Tailwind's CLI can compile static CSS during a build, without requiring React, Vite, or JavaScript in the browser. Adding it to my Containerfile wouldn't necessarily be difficult.

The bigger difference is how I'd work with CSS.

Instead of writing component styles separately, I'd compose utilities directly in the markup.

I can see why that's useful, including for AI agents. Tailwind provides a predefined vocabulary rather than leaving every class name up to the developer or agent.

But I still prefer meaningful component classes over long utility lists in my templates.

And Tailwind has conventions of its own, such as making sure its source scanner can detect the utility classes being used.

I'm not convinced that switching would make my development workflow better. It would mostly make it different.

CUBE CSS

CUBE CSS is probably the approach I found most relevant to my existing setup.

It separates Composition, Utility, Block, and Exception. I particularly like the distinction between layout composition and component styling.

Unlike CSS Modules or Tailwind, I can borrow these ideas without changing my toolchain.

I can keep my stylesheets, use BEM-style names where they make sense, and organize the CSS a little more deliberately.

And CSS itself?

Part of the reason I'm comfortable keeping vanilla CSS is how much the language has improved.

Nesting, custom properties, container queries, cascade layers, and @scope all address problems that previously required additional conventions or tooling.

For example, cascade layers let me explicitly define the precedence of different groups of styles:

@layer base, components, utilities;
Enter fullscreen mode Exit fullscreen mode

And @scope lets me target elements within a particular part of the DOM without relying entirely on globally unique selectors.

It's now supported in current versions of the major browsers, although older browser support still matters. It isn't the same kind of isolation as CSS Modules either.

None of these features replaces BEM, but they give me more options for solving specific problems without introducing another toolchain.

What actually changed with AI?

After looking into these approaches, I couldn't find convincing evidence that AI agents produce more maintainable CSS with BEM, Tailwind, CSS Modules, or data attributes.

Each has plausible advantages.

Tailwind provides a shared vocabulary. CSS Modules offers local class scoping. BEM makes naming patterns predictable.

But I don't think we can conclude that one is inherently better for AI-assisted development.

What matters more to me is consistency.

If an agent works on a component that already uses BEM, it shouldn't introduce data attributes just because they look more modern.

If a component already exposes its state through aria-expanded, it shouldn't add data-state to represent the same thing.

And if the project already has design tokens, it shouldn't invent a slightly different color or spacing value for every new component.

So rather than replacing my CSS architecture, I've been making its conventions more explicit.

For one of my projects, I put the UI guidance into a repository-local Codex skill under .agents/skills/.

The skill describes existing components, where shared design tokens belong, how variants and UI states should be handled, and which stylesheets to look at. It also points to the component gallery, so an agent has actual examples rather than just abstract rules.

The relevant conventions can be summarized as:

## Component styling

- Follow existing component and CSS conventions.
- Reuse shared design tokens.
- Check existing components before creating new ones.
- Preserve clear BEM-style names.
- Use native HTML states where appropriate.
- Reuse semantically correct ARIA states for styling.
- Avoid duplicating state across classes and attributes.
- Use data attributes when they make variants clearer.
Enter fullscreen mode Exit fullscreen mode

These are the principles rather than a verbatim extract from the skill.

In AGENTS.md, a short reference is enough:

For changes to UI templates, CSS, or visual Stimulus states,
use the repository-local UI skill.
Enter fullscreen mode Exit fullscreen mode

I like this separation. The agent has access to detailed UI guidance when it's relevant, without loading all of it for unrelated backend work.

It's not a new CSS architecture. It's a way to make the existing one easier to understand and continue.

Of course, the skill doesn't guarantee correct code. I still have to review what the agent produces.

But it reduces how much of the project's styling conventions I have to explain repeatedly.

And perhaps that's where AI has changed my approach the most. Decisions that previously lived in my head now benefit from being documented somewhere an agent can find them.

So, is BEM still worth using?

For me, yes.

I don't see a reason to replace my current CSS setup. It's comfortable, requires very little maintenance, and works well for the applications I'm building.

I'll continue using BEM where it makes sense, use data attributes when they add clarity, and take advantage of modern CSS features when they solve an actual problem.

There's also plenty to learn from approaches like CUBE CSS without adopting an entirely new system.

But the most useful result of this research wasn't discovering a better naming convention.

It was making the conventions I already use more explicit.

And I still find it amusing that the Codex agent, which prompted me to question my CSS approach in the first place, ended up arguing against changing it just for the sake of change.

I think I'll take its advice on this one.

Top comments (0)