Tech 3 min read

AI Keeps Forgetting That Astro Scoped CSS Does Not Apply to JavaScript-Generated Elements

IkesanContents

The Short Version

If you work with Next.js or Nuxt, this is probably old news. AI agents keep making the same mistake, so the easiest fix is to write it down in Claude.md, Gemini.md, or Agents.md and save yourself the trouble later.

This article is for people who are not immediately sure what that means.

The Problem

In an Astro component, a <style> tag is compiled as scoped CSS by default. That is a useful feature because it prevents style leakage between components, but there is a catch: it does not apply to DOM elements generated dynamically in JavaScript.

Why It Does Not Apply

Astro’s scoping works by analyzing HTML and CSS at build time and adding a unique attribute such as data-astro-cid-xxxxx to the relevant elements.

<!-- Before build -->
<div class="card">Hello</div>
<style>
  .card { background: blue; }
</style>

<!-- After build (conceptual) -->
<div class="card" data-astro-cid-abc123>Hello</div>
<style>
  .card[data-astro-cid-abc123] { background: blue; }
</style>

Elements created dynamically in JavaScript do not receive that attribute, so the selector no longer matches and the styles do not apply.

<div id="container"></div>

<style>
  .dynamic-item { color: red; } /* does not apply */
</style>

<script>
  const container = document.getElementById('container');
  const item = document.createElement('div');
  item.className = 'dynamic-item'; // no data-astro-cid-xxx
  item.textContent = 'Dynamic content';
  container.appendChild(item);
</script>

Workarounds

1. Use is:global

Use <style is:global> to emit global CSS instead of scoped CSS.

<style is:global>
  .dynamic-item { color: red; } /* applies */
</style>

That said, it is global, so it can affect other pages and components. Avoid collisions by using a naming convention such as BEM or a prefix, or by choosing a sufficiently unique class name.

2. Use :global()

If you only want part of the selector to be global, use the :global() pseudo-class.

<style>
  /* Scoped for static elements */
  .container { padding: 1rem; }

  /* Global for dynamic elements */
  :global(.dynamic-item) { color: red; }
</style>

3. Use inline styles

Set the style directly from JavaScript.

const item = document.createElement('div');
item.style.color = 'red';
item.style.fontSize = '14px';

This works well for simple styling, but gets hard to manage once things become complex.

4. Use Tailwind CSS

Tailwind CSS defines utility classes globally, so dynamic elements can use them without issues.

const item = document.createElement('div');
item.className = 'text-red-500 text-sm font-bold';

”Why Not Make Everything Global?”

You might think that sounds easier, but scoped CSS has real benefits:

  • No naming collisions: Generic names such as .card or .button can be reused freely in each component
  • Dead code removal: CSS for unused components disappears automatically, while global CSS requires manual cleanup
  • Clear boundaries: The styles for this component stay in this file, which is easier to reason about

If a page has no dynamic generation, scoped CSS is fine. Only pages with dynamic generation need is:global. Making everything global does not solve the problem; it throws away the benefit.

Summary

MethodScopeUse case
<style is:global>Whole pagePages with lots of dynamic generation
:global(.class)Specific classes onlyWhen only part of the UI is dynamic
Inline stylesPer elementSimple styling
Tailwind CSSGlobalProject-wide utility usage

Tell AI to do it properly.