Getting StartedComponentsDesign TokensThemingTheme SynthesizerFrameworksAccessibilityUtilitiesServer RenderingBrowser SupportContributingChangelogApp ShellAspect GridAuth ShellCenterContainerDashboard GridFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbarAnchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkStepper NavTabsTop BarTree ViewAccordionAspect RatioAvatarAvatar GroupCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSkeletonSpinnerStackVideoVirtual ListActivity HeatmapAnimated NumberBadgeBar ListChartClockComparisonCountdown TimerData GridDescription ListDiffGaugeJSON TreeKanbanLevel MeterListMeterSparklineStatStepperTagTimelineUptimeValue CardWaveformBlockquoteCode BlockCode GroupGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriterButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerField ListFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree SelectAlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuHover CardLoading OverlayNotification PanelPopoverProgressSheetToastTooltip
ARC UIARC Radiant Components
v4.9DocsComponentsTokensSynthesizer
Getting StartedFrameworksServer RenderingDesign TokensThemingTheme SynthesizerTypographyUtilitiesAll ComponentsAccessibilityBrowser SupportChangelogContributingStatsApp ShellAspect GridAuth ShellCenterContainerDashboard GridFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbarAnchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkStepper NavTabsTop BarTree ViewAccordionAspect RatioAvatarAvatar GroupCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSkeletonSpinnerStackVideoVirtual ListActivity HeatmapAnimated NumberBadgeBar ListChartClockComparisonCountdown TimerData GridDescription ListDiffGaugeJSON TreeKanbanLevel MeterListMeterSparklineStatStepperTagTimelineUptimeValue CardWaveformBlockquoteCode BlockCode GroupGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriterButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerField ListFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree SelectAlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuHover CardLoading OverlayNotification PanelPopoverProgressSheetToastTooltip

Server Rendering

ARC UI components server-render through @lit-labs/ssr intodeclarative shadow DOM: a <template shadowrootmode="open">the browser turns into a real shadow root while parsing, before any JavaScript runs.

All 195 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 195 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 patchesLitElement'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, which turns 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, willUpdateand render, and none of connectedCallback,firstUpdated, updated, or a reactive controller'shostConnected. Browser work belongs in those.

All 195 pass; none are client-only. There is no per-component support table to consult. You opt in per page, not per component.

Rendering isn't the whole promise, so a second test holds the rest: every component's docs preview, server-rendered, has to look the same before its script arrives as after. That includes the ones built from their children: breadcrumbs, navigation menus, tabs, accordions and lists arrive with their items, and nothing moves when the script lands. Two exceptions, both about the clock: arc-clock shows the time, andarc-activity-heatmap without an end-date ends today. Only the browser knows either.

Client-side routers

A router that fetches the next page and parses it with DOMParser gets declarative shadow roots as inert templates: only the page parser attaches them. Astro's<ClientRouter /> is one. Attach them before the swap:

Without it, every component on the incoming page renders a fresh copy next to a dead template, and nothing hydrates.

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-react also works, if you would rather render through the component graph.

It renders any Lit element defined in the process, not only ARC's. Import another library's registration before rendering (@arclux/brand/register, your own components) and its elements arrive rendered too, the way an icon pack's glyphs do.

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.