This error looks scary, but in SvelteKit it almost always comes down to one thing: your code touched a DOM element that wasn’t there yet. Here’s how to tell which case you’re in and how to fix it.
One note first: if this started right after you upgraded @sveltejs/adapter-node, the adapter probably isn’t the cause. Version 5.0.0 changed graceful shutdown and made precompress default to true. Neither touches how components render. Look at what else changed in the same upgrade (Svelte, SvelteKit, a component library).
Case 1: the element is undefined when you use it
This is the actual “reading ‘style’” error. A typical way to hit it:
<script>
import { onMount } from 'svelte';
let { show = false } = $props();
let el;
onMount(() => {
el.style.display = 'block'; // el is undefined if the div isn't rendered
});
</script>
{#if show}
<div bind:this={el} class="modal">Hello</div>
{/if}
When show is false, the div doesn’t exist, bind:this never runs, and el stays undefined.
The best fix is to stop poking at the DOM and let Svelte do it:
<script>
let open = $state(false);
</script>
<button onclick={() => (open = true)}>Open</button>
<div class="modal" style:display={open ? 'block' : 'none'}>Hello</div>
If you really need the element (to focus it, measure it, and so on), keep bind:this and check it first:
<script>
import { onMount } from 'svelte';
let el;
onMount(() => {
if (el) el.focus();
});
</script>
<div bind:this={el} tabindex="-1">Hello</div>
(This uses Svelte 5 syntax. On Svelte 4, use export let show and on:click. onMount and bind:this work the same way.)
Case 2: you used document during server rendering
SvelteKit renders your pages on the server by default. The top-level code in a component’s <script> runs there, and there’s no document or window in Node:
<script>
// Runs during SSR → ReferenceError: document is not defined
const modal = document.getElementById('my-modal');
</script>
Note that the error here is document is not defined, not “of undefined”. If that’s what you see, this is your case.
What does and doesn’t run on the server:
- Top-level
<script>code runs on the server and in the browser. onMountruns only in the browser, never during SSR.$effectalso runs only in the browser.
So the fix is to move DOM access into onMount (or $effect), or guard it with browser:
<script>
import { browser } from '$app/environment';
if (browser) {
// client-only code
}
</script>
Libraries that touch window on import
Some libraries read window or document as soon as you import them, which crashes SSR. Import them only in the browser:
<script>
import { onMount } from 'svelte';
onMount(async () => {
const { default: lib } = await import('some-browser-only-lib');
lib.init('#my-button');
});
</script>
<button id="my-button">Hover me</button>
If a whole page depends on browser APIs, you can turn SSR off for it:
// +page.js
export const ssr = false;
Catching this before production
Build and run the production server locally, then load your pages:
npm run build
node build
In CI, run the same two commands and request a real page (for example curl -f http://localhost:3000/). If a component breaks during SSR, the request fails and so does the pipeline. A lint rule that flags window and document outside onMount and browser checks also helps.
FAQ
Does onMount run on the server?
No. It runs only in the browser, after the component is mounted.
Why undefined in one case and document is not defined in another?
undefined means a variable exists but holds nothing (like an unbound bind:this). document is not defined means the browser global doesn’t exist at all, which is what happens in Node during SSR.
Should I use bind:this or document.getElementById?
Prefer bind:this. It’s scoped to the component and doesn’t depend on a global lookup. Prefer reactive state over both when you just want to show, hide, or style something.
Wrapping up
When you see this error, ask two questions: is the element actually rendered when my code runs, and is my code running on the server? Keep DOM work inside onMount, $effect, or browser checks, and let Svelte’s reactivity handle the rest.
If you’ve hit this error in a weirder way, tell me about it in the comments.

