Server Rendering
Render initial HTML on the server and add interactions in the browser.
ui-sdk parts render HTML with Solid's server renderer. Their initial attributes include ARIA state and the group's tab stop.
Render the Initial State
import { Toggle } from "@ui-sdk/solid/toggle"
function Example() {
return <Toggle defaultPressed>Bold</Toggle>
}
export { Example }The server sends a button with aria-pressed="true". It can toggle after hydration. Native HTML behavior, such as submitting a form, can work before hydration.
Read Browser Values After Mount
The server does not know the browser’s language. Start with the same placeholder on the server and client, then read the language after mount:
import { createSignal, onSettled } from "solid-js"
function Example() {
const [language, setLanguage] = createSignal("")
onSettled(() => {
setLanguage(navigator.language)
})
return <p>{language() || "Loading language…"}</p>
}
export { Example }Both initial renders show “Loading language…”. Once mounted, the paragraph shows the browser’s language. Read other browser-only values in the same place, or in an event handler.
Saved Preferences and Storage
Preferences like a color scheme or an emoji skin tone are saved in the browser, so the server can't render them. Preferences covers the preferences themselves; this is how they fit server rendering.
Show Them Before Hydration
Render Preferences around your app. On the server it renders one inline script as its first child, which puts every saved value on its data-* attribute as the page loads, before Solid hydrates, so CSS that selects those attributes is right from the first paint:
import { Preferences } from "@ui-sdk/solid/preferences"
import type { JSX } from "@solidjs/web"
import { colorScheme, skinTone } from "./preferences"
function App(props: { children: JSX.Element }) {
return (
<Preferences preferences={[colorScheme, skinTone]}>
{props.children}
</Preferences>
)
}
export { App }A preference made with global: true goes on <html> instead. Pass nonce if your Content Security Policy only allows inline scripts with one.
Content that reads a preference through usePreference renders the default on the server and the saved value once mounted. Style by the attribute instead wherever the first paint has to be right, as Style by Preferences shows.
Read a Cookie on the Server
A preference kept in a cookie can render on the server too, content included. Read the request's Cookie header on the server, however your server or framework gives you the request, and pass it to your app as a prop, like renderToString(() => <App cookie={cookie} />). Create the preference inside the app, so each request gets its own, and share it through context:
import { createCookieStorage, createPreference } from "@ui-sdk/core"
import { Preferences } from "@ui-sdk/solid/preferences"
import type { JSX } from "@solidjs/web"
import { createContext } from "solid-js"
const createSkinTone = (cookie: string | null) =>
createPreference({
values: ["default", "light", "medium", "dark"],
defaultValue: "default",
attribute: "data-skin-tone",
storage: createCookieStorage("skin-tone", { cookie }),
})
const SkinToneContext = createContext<ReturnType<typeof createSkinTone>>()
function App(props: { cookie: string | null; children: JSX.Element }) {
const skinTone = createSkinTone(props.cookie)
return (
<SkinToneContext value={skinTone}>
<Preferences preferences={[skinTone]}>{props.children}</Preferences>
</SkinToneContext>
)
}
export { App, SkinToneContext }The server reads the value from cookie, and the browser from document.cookie, so both render it and hydration matches.
Render the Storage Script Yourself
Preferences is the usual way. For one component that keeps its own value, like a toolbar's density, a storage's script returns the same kind of inline script for a single element. Render it right after the element, since it writes to the element right before it:
import { createLocalStorage } from "@ui-sdk/core"
import { isServer, type JSX } from "@solidjs/web"
import { createEffect, createSignal } from "solid-js"
const density = createLocalStorage("density")
const script = density.script({
attribute: "data-density",
values: ["comfortable", "compact"],
})
function Toolbar(props: { children: JSX.Element }) {
let element: HTMLDivElement | undefined
// The script is in the server's HTML until then.
const [mounted, setMounted] = createSignal(false, { ownedWrite: true })
createEffect(
() => 0,
() => {
element?.setAttribute(
"data-density",
density.get() === "compact" ? "compact" : "comfortable"
)
setMounted(true)
}
)
return (
<>
{/* Only the server renders the attribute; the effect sets it in the browser. */}
<div
{...(isServer ? { "data-density": "comfortable" } : {})}
ref={(el: HTMLDivElement) => (element = el)}
>
{props.children}
</div>
{mounted() || !script ? null : <script innerHTML={script} />}
</>
)
}
export { Toolbar }- Only from the server. The browser never runs a script Solid inserts, so drop it once mounted.
- Set the attribute yourself. Render it on the server only and set it from an effect, as above, so hydration never puts the server's value back over the saved one.
- Shared by ID. Every script with the same storage kind and ID reads the storage once, however many elements render it, and the scripts are identical, so compression shrinks the repeats.
- Only safe values. Any script on your site can write the saved value, so
scriptonly sets attributes that start withdata-, and only to one ofvalues. - Not for cookies. A cookie storage's
scriptreturns undefined, since the server reads the cookie and renders the value itself.
To run before the page's content at all, render preferencesScript([...]) from @ui-sdk/core in <head> yourself. Only global preferences make sense there, since the others go on the script's parent element.
Keep IDs and Values Consistent
Use the same initial component values on the server and client. For example, do not render an open dialog on the server and a closed one for the first client render.
Parts generate IDs for relationships such as a Dialog trigger’s aria-controls and a Field label’s for. You usually do not need to supply them.
Pass an explicit id when something outside the component needs to refer to a part. On the server, render order matters: if a Field label renders before a control with your ID, the label initially uses the generated ID. Hydration updates the reference to yours. The same applies to Dialog triggers, titles, and descriptions.
Use classes or data attributes for styling, rather than generated IDs.
Popups render in place, so your server layout doesn't need a portal root. Popups covers containers; Forms covers submission and validation.