Client-Side Inserted HTML Ignores Your Astro Styles: The Scoped CSS + innerHTML Gotcha

TL;DR — Astro scopes a component's <style> block at compile time by stamping a data-astro-cid-XXXXXXXX attribute onto every element it server-renders, then rewriting selectors to require that attribute. HTML built client-side — insertAdjacentHTML, innerHTML, template literals — never gets that attribute, so scoped styles silently never match it. No error, no warning; the markup looks correct on inspection, it just falls back to bare browser defaults.
Table of Contents
Three times this bit the same site
1. Infinite-scroll post cards
New post cards loaded on scroll were built as HTML strings and appended via insertAdjacentHTML. The scoped .feed-card/.avatar rules never matched them — visually, a byline avatar rendered at its native pixel size instead of the intended 32×32px, a full-size image blowing up the card layout.
2. Upload preview thumbnail
A logo/avatar upload preview built with document.createElement("img") then .replaceWith() lost the scoping attribute the original placeholder element had. Result: the preview rendered at full original size instead of the compressed thumbnail size.
3. Live comment list
A comment thread rendered via a template-literal renderCmt() function and inserted with innerHTML produced unstyled comment items — less visually dramatic than the other two since comment avatars were colored initial-letter divs, not oversized <img> tags, so it just looked like plain unstyled text rather than an obviously broken layout.
The fix
- <style>
+ <style is:global>
.feed-card {If the block used Astro's :global(...) escape hatch to reach into child markup from inside a scoped block, drop the wrapper once the whole block is already global — leaving it in can produce a selector that matches nothing.
- .post-card-image :global(img) {
+ .post-card-image img {For an element that already exists server-rendered and only needs its attributes swapped (e.g. a preview image's src), update it in place instead of replacing the node — that keeps its original data-astro-cid-* attribute intact. If you must swap element type (a placeholder <span> becoming an <img>), copy the data-astro-cid-* attribute across before calling replaceWith().
Rule going forward
- Before writing client-side JS that appends or clones markup mirroring a server-rendered component's structure, find that component's <style> block.
- Make it is:global (simplest), or move the shared classes into an existing global stylesheet if they're broadly reused.
- Verify by actually triggering the client-side path (scroll to load more, upload a file, post a comment) and looking at the result — this bug produces zero errors anywhere, a visual check is the only way to catch it.




Comments