On This Page
Server Side Rendering
Purpose of SSR
When a webpage is loaded custom tags are upgraded, this means to have content on initial load that matches the final styling you will need to use an SSR solution.
Without SSR you will see a flash of unstyled content when the UI component is upgraded to its final rendered shadow DOM.

Server rendering sends the component’s shadow DOM with the page, so the browser paints it while parsing rather than waiting for the component to upgrade.
For how the server output and client hydration fit together see the server rendering API reference.
Server Adapters
Most projects reach for an adapter rather than rendering components by hand. Each one finds registered tags in your HTML and expands them into Declarative Shadow DOM.
| Package | Use it for |
|---|---|
@semantic-ui/server |
Any server, including Express, Hono, and node:http |
@semantic-ui/vite |
Vite |
@semantic-ui/eleventy |
Static sites |
@semantic-ui/astro |
Astro |
Your client bundle needs no hydration code. A component detects the server’s shadow DOM on upgrade and wires itself.
Compiling Ahead An adapter means you already have a build, so templates and specs can be compiled during it rather than in the browser.
Rendering on the Server
The adapters are built on renderToString, exported from @semantic-ui/component/server, the package’s server entry. The root entry stays the browser’s, so a client bundle carries none of the server rendering. renderToString takes a component and its attributes and returns the element with its shadow DOM inlined.
import { defineComponent } from '@semantic-ui/component';import { renderToString } from '@semantic-ui/component/server';
const Card = defineComponent({ tagName: 'my-card', template, css });
const html = renderToString(Card, { title: 'Hello' });The output wraps your rendered content in a <template shadowrootmode="open"> which the HTML parser converts into a real shadow root.
<my-card title="Hello"> <template shadowrootmode="open"> <style>/* component css */</style> <div class="card">Hello</div> </template></my-card>Nested components are expanded for you. A template containing <ui-icon> produces that icon’s shadow DOM in the same pass.
Server-Only Output
Pass hydrate: false for markup that should never become interactive.
const html = renderToString(Card, { title: 'Hello' }, { hydrate: false });The element receives an ssr attribute and the client leaves it alone.
Rendering Static Markup
Some destinations never run a component’s JavaScript: an email, a feed, a static page. renderToStaticMarkup renders the same component to the markup of its template alone, leaving out the shadow root, the style and the hydration markers.
import { defineComponent } from '@semantic-ui/component';import { renderToStaticMarkup } from '@semantic-ui/component/server';
const html = renderToStaticMarkup(Card, { title: 'Hello' });// <div class="card">Hello</div>Slot content passes in as slots and fills in where each {>slot} sits. Nested components render in place. A definition without a tagName renders the same way, so an email or a snippet needs no custom element. Pass text: true for a plain text version with nothing escaped, and css: true to receive { html, css } with the css of every component used, ready for an inliner.
@semantic-ui/server exposes this as renderStatic.
Building Components for SSR
When a component is rendered on the server it is rendered in isolation so its not possible to directly access window or other parts of the DOM from your component.
Components provide an isClient and isServer boolean to callbacks to allow you to have separate code paths for the server and client.
const createComponent = ({ isServer, state }) => ({ initialize() { if (isServer) { return; } state.width.set(window.innerWidth); },});Client Only Code
Some common values that might be useful in a component but should be prevented from running on the server
- Accessing
navigator.userAgentto determine browser type - Accessing
windowordocumentor other DOM elements - Accessing
localStorage
Timers and event listeners created through callback arguments are already inert on the server and need no guard.
Async Data
{#async} renders its loading branch on the server. The promise is never awaited because the server cannot update markup it has already sent.
{#async loadUser as user} <h2>{user.name}</h2>{loading} <div class="skeleton"></div>{/async}The skeleton ships in the HTML and the client resolves the promise after hydration. Data that must be present at first paint should arrive as a setting rather than be fetched inside the component.
Hydration
Hydration attaches reactivity to the server’s DOM instead of replacing it. Nothing re-renders and nothing is compared against what the client would have produced.
createComponent runs again on the client, so anything skipped under isServer runs then. State initialized on both sides needs to produce the same value, otherwise the first update will visibly correct it.
Components carrying markers from an older build are discarded and rendered fresh, so a deploy that changes a template cannot leave mismatched bindings behind.