Server Rendering
ARC UI components server-render through @lit-labs/ssr into
declarative shadow DOM — a <template shadowrootmode="open">
the browser turns into a real shadow root while parsing, before any JavaScript runs.
All 207 components render this way today, and all of it is optional: a consumer with no build step, or loading ARC from a CDN, needs none of it and loses nothing.
What it fixes
Most ARC components take their content from properties rather than slots. Before JavaScript loads — or without it — this is an empty unknown tag: no heading, no description, nothing for a crawler to read.
The same tag through the server renderer (Lit's hydration markers elided) — the content is in the HTML response itself:
65 of 207 components are shaped like this — no default slot, no markup to fall back to. Server rendering is the only thing that puts their content in the payload. Slot-driven components already degrade gracefully, and gain less from it.
Rendering and hydrating
On the server, render as you would any Lit template:
On the client, import the hydration entry before any component:
Without it, the moment an element upgrades Lit renders its template from scratch into
the shadow root the server already filled — discarding identical DOM and producing the
exact flash server rendering exists to avoid. With it, Lit adopts the existing
nodes and renders nothing. Order matters because hydration support patches
LitElement's update path: a component class defined before the patch never
receives it.
The FOUC guard
base.css hides ARC elements until they upgrade, so a page never flashes
unstyled custom elements. A server-rendered element is un-upgraded but finished,
so that rule would hide completed content for the whole hydration window — turning server
rendering into a longer blank screen rather than a shorter one.
Opt the page out at the root:
This is a root-level switch rather than something automatic because CSS cannot detect a
declarative shadow root: the browser consumes the <template> during
parsing and leaves no selectable trace of it.
Component coverage
pnpm check ssr renders every component in Node and fails on any that
throws; it runs in CI. Server-side, Lit runs the constructor, willUpdate
and render — and none of connectedCallback,
firstUpdated, updated, or a reactive controller's
hostConnected. Browser work belongs in those.
All 207 pass; none are client-only. There is no per-component support table to consult — you opt in per page, not per component.
Framework support
Lit's framework SSR integrations work from the component graph, which is why only React
ever had one. But nothing about the problem needs the graph: a framework's server render
produces HTML, and every <arc-*> in it can be rendered to a
declarative shadow root from the markup alone. So @arclux/arc-ui/ssr takes
HTML and returns HTML, and does not care what produced it.
- Nuxt, SvelteKit, Angular Universal, Next, Astro, or a string you built by hand — pipe the rendered HTML through it. This is the code that renders this site: 177 pages, 43,620 shadow roots, every build.
- React —
@lit-labs/ssr-reactalso works, if you would rather render through the component graph.
It needs @lit-labs/ssr, an optional peer dependency — install it in the
project that server-renders. Two things are on the caller: serve the returned
stylesheets from the path they were linked with, and import
@arclux/arc-ui/hydrate on the client before any component is
defined — on a bundler that usually means forcing it into its own chunk, since an
import statement alone does not control evaluation order.