AI Keeps Forgetting That Astro Scoped CSS Does Not Apply to JavaScript-Generated Elements
Contents
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
.cardor.buttoncan 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
| Method | Scope | Use case |
|---|---|---|
<style is:global> | Whole page | Pages with lots of dynamic generation |
:global(.class) | Specific classes only | When only part of the UI is dynamic |
| Inline styles | Per element | Simple styling |
| Tailwind CSS | Global | Project-wide utility usage |
Tell AI to do it properly.