Server rendering
ranui components serialize to declarative shadow DOM, so a server can emit the real markup and the first paint is correct before any JavaScript runs.
Use when you render pages on a server or at build time (an SSG, an Express/Hono/Workers route, an email-preview job) and want
<r-*>elements to arrive as visible markup rather than as empty tags waiting for hydration.
Quick Start
import 'ranui'; // populates the SSR registry — do this first
import { renderHTMLToString } from 'ranui/ssr-stream';
const html = await renderHTMLToString(`
<r-button type="primary">Submit</r-button>
<r-progress percent="65"></r-progress>
`);Each registered <r-*> tag is instantiated, its attributes applied, its children rendered
recursively, and the result emitted with a <template shadowrootmode="closed"> inside. Unknown
tags pass through untouched, so it is safe to run over a whole page of ordinary HTML.
Streaming
renderToStream is the same renderer as an async generator, so static chunks reach the client
while later components are still rendering:
import { renderToStream } from 'ranui/ssr-stream';
for await (const chunk of renderToStream(pageHtml)) response.write(chunk);One component at a time
ranui/ssr renders an instance you constructed yourself, useful when you are assembling a
tree in Node rather than templating a string:
import { renderToString } from 'ranui/ssr';
import { Button } from 'ranui';
const html = renderToString(new Button());API Reference
| Export | Entry | Signature | Description |
|---|---|---|---|
renderHTMLToString(html) |
ranui/ssr-stream |
(html: string) => Promise<string> |
Expands every registered <r-*> tag in an HTML string. |
renderToStream(html) |
ranui/ssr-stream |
(html: string) => AsyncGenerator<string> |
The same, chunk by chunk. |
renderToString(el) |
ranui/ssr |
(component) => string |
Serializes one component instance. |
RanElement |
ranui/ssr |
class | HTMLElement in a browser, the SSR mock in Node. |
h(tag, props, …children) |
ranui/ssr |
(tag, props?, ...children) => string |
Small helper for hand-building markup. |
What the server can and cannot do
The client rebuilds; it does not reuse. ranui attaches closed shadow roots, and
attachShadow on an element that already has a declarative shadow root removes that root's
children when the mode is closed. So the server-rendered tree paints the first frame and is
then replaced by an identical client-built one. Two consequences:
- You get correct first paint, not hydration reuse: closed shadow roots cannot be reused by the client, for the reason above. See the coding guidelines.
- Never put state in the server-rendered shadow markup expecting the client to read it back. Pass it through attributes, which survive.
Nothing measured exists on the server. Anything that depends on getBoundingClientRect
or offsetWidth resolves after mount, in the browser. Components are written so their initial
layout comes from CSS for exactly this reason.
Four elements do not server-render today, each because it accesses a browser API in its
constructor: <r-content> (MutationObserver), <r-link> (document), <r-modal> (a slot
method the SSR mock does not implement) and <r-radar> (ResizeObserver). They pass through
as plain tags and upgrade on the client. Every other element is covered by a test that fails
if it stops rendering, so this list cannot grow silently.
Theming and flash
initTheme() is a no-op on the server (all document / localStorage / matchMedia access
is guarded), so the theme is applied by the client. To avoid a flash of the wrong theme, set
data-ran-theme on <html> in your server template (from a cookie, or from a tiny inline
script that reads localStorage before first paint) and let
initTheme take over afterwards.
Best Practices
- Import
ranui(or the specificranui/<component>entries) before rendering. The registry is populated by the import's side effect; without it every tag passes through unexpanded and the page silently loses its markup. - Render the page, not the fragment.
renderHTMLToStringis safe over arbitrary HTML, so there is no need to isolate the ranui parts. - Ship the stylesheet. DSD markup carries the component's styles, but page-level tokens
come from
ranui/style(andranui/fontsfor the typefaces).