---
title: Server-Side Rendering
description: Guidelines and patterns for SSR and hydration safety in Moraine and SolidStart.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Server-Side Rendering

> Guidelines and patterns for SSR and hydration safety in Moraine and SolidStart.

Moraine supports SolidJS server rendering and hydration. Your application must still render the
same initial state on the server and client, defer browser-only work, and create conditional JSX
only when its branch renders.

This guide covers IDs, portals, responsive state, and content ownership.

## Deterministic Hydration IDs

Accessible components require matching IDs between elements (for example, connecting a trigger's `aria-controls` to a content panel's `id`). When server rendering, randomly generated IDs could differ between server and client, causing hydration mismatches.

Use `createId` from `moraine/utils` to generate matching IDs across server rendering and client hydration:

```tsx
import { createId } from 'moraine/utils'

function CustomField() {
  const id = createId() // Generates stable, synchronized IDs across SSR and hydration

  return (
    <div>
      <label for={id()}>Username</label>
      <input id={id()} type="text" />
    </div>
  )
}
```

## Hydration-Safe Portals

Floating overlays—such as `Dialog`, `Sheet`, `Popover`, `Tooltip`, and `ContextMenu`—frequently render into `document.body` via a Solid `<Portal>`.

In Moraine:

- Overlays default to closed states, rendering nothing or minimal placeholders during initial server rendering.
- When an overlay opens, Moraine delays portal insertion until hydration completes in the browser, preventing server/client DOM mismatch.
- Read browser APIs in `onMount` or event handlers. For browser-only content, keep the server and
  initial client tree identical, then reveal that content after mounting.

```tsx
import { Button, Dialog } from 'moraine'

export function UserDialog() {
  return (
    <Dialog>
      <Dialog.Trigger as={Button}>Edit Profile</Dialog.Trigger>
      {/* Content safely portals to body on client without hydration error */}
      <Dialog.Content title="Edit Profile">
        <p>Update your profile details below.</p>
      </Dialog.Content>
    </Dialog>
  )
}
```

## Conditional JSX and item content

Create JSX inside the branch that renders it. For collection items with optional panels, use a
`content` getter so an inactive panel does not consume hydration IDs before it mounts:

```tsx
import { Tabs } from 'moraine'

export function SettingsTabs() {
  return (
    <Tabs
      items={[
        {
          value: 'account',
          label: 'Account',
          get content() {
            return <p>Account settings</p>
          },
        },
        {
          value: 'notifications',
          label: 'Notifications',
          get content() {
            return <p>Notification settings</p>
          },
        },
      ]}
    />
  )
}
```

The same rule applies to JSX panel data in [Accordion](https://moraine.subf.dev/components/accordion.md) and
[Stepper](https://moraine.subf.dev/components/stepper.md). Keep server and client item order and initial selection equal.

## Responsive Queries in SSR

Browser APIs like `window.matchMedia` do not exist on the server. If a component conditionally renders layout based on screen width, it must provide a deterministic server default.

Moraine's `createMediaQuery` accepts a `defaultValue` to define the fallback state during SSR:

```tsx
import { createMediaQuery } from 'moraine/utils'
import { Show } from 'solid-js'

export function Navigation() {
  // Defaults to false on the server; synchronizes with matchMedia on the client
  const isDesktop = createMediaQuery('(min-width: 1024px)', false)

  return (
    <nav>
      <Show when={isDesktop()} fallback={<MobileDrawer />}>
        <DesktopNav />
      </Show>
    </nav>
  )
}
```

## Transition Presence & Exit Lifecycles

SolidJS conditional `<Show>` primitives unmount elements immediately when their condition becomes false. Moraine components use `createTransitionPresence` internally to keep exiting overlays mounted while CSS exit animations run.

During server rendering:

- Hidden or transitioning elements maintain deterministic closed states.
- Style attributes and animation variables (such as `--mo-collapsible-content-height`) resolve gracefully to safe initial values without triggering layout thrashing or browser DOM measurement calls.

## Best Practices for SSR Safety

1. **Avoid `window` or `document` in Component Body**: Read window dimensions, localStorage, or DOM properties inside `onMount` or inside user event handlers (`onClick`, `onInput`), never at component execution time.
2. **Deterministic Initial State**: For controlled signals, initialize with static defaults that the server can evaluate consistently.
3. **Use Moraine Primitives for Overlays**: Built-in primitives already manage portal scopes, scroll locks, and focus traps so you do not need custom SSR guard wrappers.

