Skip to content

SSR Setup

Problem

@vielzeug/orbit uses browser APIs (getBoundingClientRect, ResizeObserver, etc.) that are not available on the server. Importing the main package in an SSR bundle causes build-time errors or runtime crashes.

Solution

Use the @vielzeug/orbit/ssr sub-path to get no-op stubs with the same TypeScript signatures that perform no DOM operations.

Vite / Rollup Alias

The simplest approach is a build-time alias that swaps the import when bundling for SSR:

ts
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  resolve: {
    alias: process.env.SSR ? { '@vielzeug/orbit': '@vielzeug/orbit/ssr' } : {},
  },
});

Manual Import Guard

If you need both SSR and client functionality in the same file, import conditionally:

ts
import type { FloatHandle, FloatOptions, ReferenceElement } from '@vielzeug/orbit';

async function getFloat() {
  if (typeof window === 'undefined') {
    return (await import('@vielzeug/orbit/ssr')).float;
  }
  return (await import('@vielzeug/orbit')).float;
}

What the Stubs Return

ExportSSR Return Value
computePosition{ x: 0, y: 0, placement, middlewareData: {} }
autoUpdate() => void (no-op cleanup); update is never called
computePositionAsyncPromise<{ x: 0, y: 0, placement, middlewareData: {} }> (resolves immediately)
floatFloatHandle with no-op update; getPosition() returns null; disposed tracked

The stub FloatHandle is fully typed — consuming code that calls handle.dispose() will work identically on the server and the client. disposed is correctly tracked: it is false before dispose() and true after.

SvelteKit Example

ts
// src/lib/orbit.ts
import type { float as FloatFn } from '@vielzeug/orbit';

let floatImpl: typeof FloatFn;

export async function loadFloat() {
  if (typeof window === 'undefined') {
    floatImpl = (await import('@vielzeug/orbit/ssr')).float;
  } else {
    floatImpl = (await import('@vielzeug/orbit')).float;
  }
  return floatImpl;
}
svelte
<!-- Tooltip.svelte -->
<script lang="ts">
  import { onDestroy, onMount } from 'svelte';
  import type { FloatHandle } from '@vielzeug/orbit';
  import { flip, offset, shift } from '@vielzeug/orbit';
  import { loadFloat } from '$lib/orbit';

  let triggerEl: HTMLElement;
  let tooltipEl: HTMLElement;
  let handle: FloatHandle | null = null;

  onMount(async () => {
    const float = await loadFloat();
    handle = float(triggerEl, tooltipEl, {
      placement: 'top',
      middleware: [offset(8), flip(), shift({ padding: 6 })],
    });
  });

  onDestroy(() => handle?.dispose());
</script>

<button bind:this={triggerEl}>Hover me</button>
<div bind:this={tooltipEl} role="tooltip">Content</div>

Pitfalls

  • Stubs always return zero-coordinate positions — do not run layout-dependent logic on the server using the stub's return value.
  • Dynamic imports add async overhead — prefer the build-time alias when possible; reserve the async guard for edge cases.