---
name: ssr-compatibility
description: >-
  Answers why an AdiaUI component crashes, drops content, renders wrong, or
  mutates a byte-identical subtree under SSR (linkedom/Astro), the known
  failure shapes, what's fixed vs open, how to prove a fix under the
  linkedom shim gate. Use for "does this work under SSR", why a component
  crashes on attachInternals/ResizeObserver/adoptedStyleSheets/matchMedia/
  `instanceof Node` under a DOM shim, why table-ui/chart-ui/select-ui or a
  container CE renders empty or drops nested children server-rendered,
  whether getBoundingClientRect() is safe in connectedCallback, whether a
  custom render()/connected() path adopts-in-place or rebuilds a
  server-rendered subtree that already matches (zero-subtree-mutation /
  AC-004a-shaped asks), whether a querySelector-guard-before-innerHTML
  component is SSR-safe, or whether a shim can be deleted after a fix
  ships. ANSWERS only. NOT for a fix (primitive-authoring) or
  host/hydration wiring (host-wiring, adia-ui-factory).
disable-model-invocation: false
user-invocable: false
---

# ssr-compatibility, does it work under SSR, and why not if it doesn't

Two real consumers (adiav2's `admin-portal-fe` and `factory-dashboard`) server-render
AdiaUI's light-DOM components via Astro 5 + `custom-elements-ssr`, which runs on
linkedom, a DOM shim with no layout engine and missing browser APIs the framework's
base class assumes exist. Every SSR bug that's surfaced maps onto one of six root-cause
shapes; misclassifying a new report against the wrong shape sends the investigation
to the wrong fix (or worse, invents a redundant one). This pack answers "which shape is
this" and "what's the state of each shape's fix", it never carries the fix itself.

## The shapes, in one line each

1. **A browser-only API is called unconditionally → crash** (`attachInternals`, Observers, `adoptedStyleSheets`). **Fixed** (gh#285).
2. **`connectedCallback` destructively re-stamps existing DOM → silent content loss.** ORIGINAL diagnosis, narrowed to a static audit 2026-07-17 (gh#284), doesn't currently expose against any shipped component.
2b. **Custom-element upgrade doesn't replay `attributeChangedCallback` for pre-existing attributes → reflected properties stuck at class default.** The REAL mechanism behind gh#284's symptom. **Fixed 2026-07-18** (PR #309).
3. **A connect-time layout measurement is treated as confirmed, not unknown.** **Fixed for one component** (gh#286); the general pattern is unswept.
4. **Property-only components can't seed initial state from SSR HTML** (gh#288), a feature gap, **CLOSED 2026-07-18** (table-ui's `data="[…]"` attribute).
5. **A custom `render()`/`connected()` unconditionally rebuilds a subtree that already matches**, no value-diff, no adopt-existing-DOM path, so a byte-identical server-rendered subtree gets torn down at upgrade. **Fixed for table-ui** (PR #1756, gh#1678, merged 2026-08-20); not swept framework-wide (no static audit for it).
6. **The conditional-inject class** (`querySelector`-guard-before-`innerHTML`, `search-ui`/`pagination-ui`), decision recorded: SSR-safe when the guard target is structurally specific; post-adopt attribute writes still need shape 5's idempotent-write guard, **not yet applied** to either component (gh#1678's follow-up).

Full symptom → root-cause → status detail, cited to the actual shipped/open
code: [failure-shapes.md](references/failure-shapes.md).

## Consult table

| Ask | Answer from |
| --- | --- |
| "why does `<text-ui>`/`<avatar-ui>`/a container lose its content under SSR" | [`failure-shapes.md`](references/failure-shapes.md) §2, shape 2, NARROWED (doesn't currently reproduce against any shipped component; see the survey before assuming a new report fits this shape) |
| "why does this crash / throw at import, construction or connect under SSR" | [`failure-shapes.md`](references/failure-shapes.md) §1, shape 1, FIXED twice (gh#285, gh#1430/#1436) and now GATED by `scripts/dev/ssr-linkedom-smoke.mjs`; the exact guard shape to copy for a NEW instance is [`guard-patterns.md`](references/guard-patterns.md) §1, feature-detect the API, never `typeof window` |
| "is this connect-time `getBoundingClientRect()`/measurement read safe" | [`failure-shapes.md`](references/failure-shapes.md) §3 + [`guard-patterns.md`](references/guard-patterns.md) §3, the fixed component's exact shape, and the "unknown ≠ confirmed" principle to apply elsewhere |
| "what's fixed vs still open for SSR support" | [`status-ledger.md`](references/status-ledger.md), re-verify against `gh issue view` before trusting it, it drifts |
| "how do I test / prove an SSR gap or fix" | [`test-without-linkedom.md`](references/test-without-linkedom.md), run the linkedom shim gate first (`node scripts/dev/ssr-linkedom-smoke.mjs`, the consumer's exact global surface), then the unit-level delete/try/finally pattern and what it does NOT prove |
| "what's the consumer's current workaround, and can they drop it yet" | [`consumer-workarounds.md`](references/consumer-workarounds.md) |
| "table/chart/select renders empty in the SSR response" | [`failure-shapes.md`](references/failure-shapes.md) §4, shape 4, CLOSED (table-ui's `data="[…]"` attribute); check whether the reporting component is registered server-side first if it still reproduces |
| "a byte-identical SSR subtree gets rebuilt/mutated at upgrade, role/tabindex/cells added that weren't in the SSR HTML" | [`failure-shapes.md`](references/failure-shapes.md) §5, shape 5, fixed for table-ui (PR #1756, gh#1678, merged 2026-08-20); the adopt-or-diff pattern to copy for a NEW instance is [`guard-patterns.md`](references/guard-patterns.md) §4, `Node.isEqualNode()` for a positional child, a seeded keyed-reconcile map for a flat list, `setAttrIfChanged()` for a plain attribute write, and §4.1 for why a renderer-owned candidate (one that can attach a listener) must never be adopted on structural equality alone |
| "is this querySelector-guard-before-innerHTML component (search-ui/pagination-ui shape) SSR-safe" | [`failure-shapes.md`](references/failure-shapes.md) §6, decision recorded: yes, when the guard target is structurally specific; post-adopt attribute writes are a separate, NOT-yet-closed gap (same file, same section) |

## Deviation doctrine

Every fix pattern this pack cites ([guard-patterns.md](references/guard-patterns.md))
carries the reasoning for why it looks the way it does (e.g. the no-op
`ElementInternals` shim exists because `undefined` would relocate the crash,
not remove it). If a new case doesn't fit an existing pattern's reasoning,
that's a signal to design a new pattern, route through `primitive-authoring`,
don't force-fit the nearest existing shape.

## Boundaries

- **Implementing any fix, new or matching an existing pattern.** Route to
  `primitive-authoring`. This pack orients and cites; it never edits
  `packages/web-components/core/` or any component source.
- **Consumer-side SSR/hydration host wiring** (an app's own Astro/Next config, which
  rendering mode to pick, how to structure the hydration boundary). Route to
  `host-wiring` (adia-ui-factory plugin), this pack answers whether the FRAMEWORK's own
  components are SSR-safe, not how a consumer app should be structured around them.
- **A2UI runtime/generation-pipeline SSR concerns** (if any surface later). Route to
  `a2ui-maintenance`: this pack is scoped to `UIElement`'s own lifecycle and the
  component/trait/module layer, not the generative pipeline.
- **Not a general web-components-SSR tutorial.** Every claim here is cited to this
  specific framework's actual code, issues, and PRs, general SSR/custom-elements
  theory that isn't grounded in this repo's own history doesn't belong in this pack.

## Worked example, the answer contract

A "content vanished" report can match shape 2's symptom while shape 2
itself no longer applies (it was narrowed 2026-07-17), the general lesson
(grep the component's `static template` before answering "known issue,
shape 2") plus a full worked ask/answer are in
[failure-shapes.md](references/failure-shapes.md) §2's own Worked example.

## Corpus of record

Routing-eval corpus: [`evals/routing-corpus.json`](evals/routing-corpus.json).
This pack is hand-authored from verified session work (not a research-wave corpus), re-sync it per [`status-ledger.md`](references/status-ledger.md)'s own instructions
whenever an issue referenced here changes state, rather than letting the ledger and
reality drift apart silently.
