As your Svelte application grows beyond simple component trees, you'll need ways to reach outside those boundaries—managing document head tags, listening to browser events, and gracefully handling errors across your app. Special elements in Svelte provide exactly that: low-friction access to global concerns that would otherwise require manual DOM manipulation or context plumbing through layers of components.
In this guide, we'll explore the four main special elements—svelte:head, svelte:window, svelte:document, svelte:body, and svelte:boundary—and show you patterns that actually work in production, along with the pitfalls we've hit and how to fix them.
svelte:head for meta tags and document metadata
The most common use of special elements is injecting tags into the document head. Whether you're setting page titles, adding Open Graph tags for social sharing, or injecting analytics snippets, svelte:head lets you do this from inside your components without manually querying the DOM.
Here's a practical pattern for a product detail page in an e-commerce SvelteKit app:
<script>
import { page } from '$app/stores';
export let product = {
id: '123',
name: 'Premium Wireless Headphones',
description: 'High-quality audio with noise cancellation',
price: 299.99,
image: 'https://cdn.example.com/headphones.jpg'
};
</script>
<svelte:head>
<title>{product.name} — Our Store</title>
<meta name="description" content={product.description} />
<meta property="og:title" content={product.name} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={product.image} />
<meta property="og:price:amount" content={product.price} />
<link rel="canonical" href="https://example.com/products/{product.id}" />
</svelte:head>
<main>
<h1>{product.name}</h1>
<p>${product.price.toFixed(2)}</p>
<p>{product.description}</p>
</main>The key insight here: svelte:head works reactively. When the component's props change, Svelte updates the head tags automatically. This is essential for SSR—on the server, these tags land in the actual HTML sent to the browser, so search engines and social networks see the right metadata immediately.
One pattern we use frequently is centralizing head management in a layout. Instead of repeating Open Graph setup in every product page, you can create a reusable component:
<!-- lib/components/SocialMeta.svelte -->
<script>
export let title = '';
export let description = '';
export let image = '';
export let url = '';
</script>
<svelte:head>
<title>{title} — Our Store</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" content={url} />
<link rel="canonical" href={url} />
</svelte:head>Then on any page:
<script>
import SocialMeta from '$lib/components/SocialMeta.svelte';
export let product;
export let canonicalUrl;
</script>
<SocialMeta
title={product.name}
description={product.description}
image={product.image}
url={canonicalUrl}
/>
<!-- page content -->svelte:window for global event handlers
When you need to listen to browser events that don't originate from a specific DOM element—resize events, scroll, beforeunload, keyboard shortcuts—svelte:window is your tool. It binds directly to the window object and automatically handles cleanup.
Here's a real example from a trading dashboard we built. We need to save the user's viewport state and warn them before they close the tab if they have unsaved changes:
<script>
import { browser } from '$app/environment';
let innerWidth = 0;
let innerHeight = 0;
let hasUnsavedChanges = false;
function handleResize() {
// Debounce or throttle in production; this is simplified
innerWidth = window.innerWidth;
innerHeight = window.innerHeight;
}
function handleBeforeUnload(event) {
if (hasUnsavedChanges) {
event.preventDefault();
event.returnValue = '';
return '';
}
}
function handleKeydown(event) {
// Cmd+S or Ctrl+S saves
if ((event.metaKey || event.ctrlKey) && event.key === 's') {
event.preventDefault();
saveChanges();
}
}
async function saveChanges() {
// API call
hasUnsavedChanges = false;
}
</script>
<svelte:window
on:resize={handleResize}
on:beforeunload={handleBeforeUnload}
on:keydown={handleKeydown}
bind:innerWidth
bind:innerHeight
/>
<div>Viewport: {innerWidth} × {innerHeight}</div>
<div>{hasUnsavedChanges ? '⚠️ Unsaved changes' : '✓ All saved'}</div>Notice the bindings: bind:innerWidth and bind:innerHeight. Svelte automatically updates these when the window resizes. This is cleaner than manually calling addEventListener and managing unsubscription in onDestroy.
svelte:document and svelte:body for document-level concerns
svelte:document and svelte:body work similarly to svelte:window but target the document and body elements respectively. Use them when you need to attach classes, respond to document-level events, or manage full-screen state.
In a SaaS CRM we maintain, we toggle a dark mode class on the body element. Rather than manually querying and modifying the body in a store subscribe, we use the special element:
<!-- src/routes/+layout.svelte -->
<script>
import { theme } from '$lib/stores/theme';
</script>
<svelte:body class:dark-mode={$theme === 'dark'} />
<slot />
<style>
:global(body.dark-mode) {
background-color: #1a1a1a;
color: #f0f0f0;
}
</style>This ensures the class is present server-side too (important for preventing flash of unstyled content in SSR), and it's reactive—when the user toggles the theme, the class updates instantly.
For document-level scroll events in a multi-panel layout:
<script>
let scrollDepth = 0;
function handleScroll() {
const doc = document.documentElement;
scrollDepth = (window.scrollY / (doc.scrollHeight - window.innerHeight)) * 100;
}
</script>
<svelte:document on:scroll={handleScroll} />
<div class="progress-bar" style="width: {scrollDepth}%"></div>svelte:boundary for error handling
Introduced more recently, svelte:boundary provides a way to catch errors in child components and prevent them from crashing your entire app. Think of it as a try-catch for your component tree.
In a ride-hailing dashboard with multiple widgets, any one widget throwing an error shouldn't take down the whole page. Here's how to protect against that:
<!-- src/routes/dashboard/+page.svelte -->
<script>
import RideMap from '$lib/components/RideMap.svelte';
import RideList from '$lib/components/RideList.svelte';
import EarningsChart from '$lib/components/EarningsChart.svelte';
let mapError = null;
let listError = null;
let chartError = null;
function handleMapError(event) {
mapError = event.detail;
}
function handleListError(event) {
listError = event.detail;
}
function handleChartError(event) {
chartError = event.detail;
}
</script>
<main>
<section>
<svelte:boundary onerror={handleMapError}>
{#if mapError}
<div class="error-fallback">
<p>Map failed to load</p>
<button on:click={() => (mapError = null)}>Retry</button>
</div>
{:else}
<RideMap />
{/if}
</svelte:boundary>
</section>
<section>
<svelte:boundary onerror={handleListError}>
{#if listError}
<div class="error-fallback">
<p>Ride list unavailable</p>
</div>
{:else}
<RideList />
{/if}
</svelte:boundary>
</section>
<section>
<svelte:boundary onerror={handleChartError}>
{#if chartError}
<div class="error-fallback">
<p>Earnings chart unavailable</p>
</div>
{:else}
<EarningsChart />
{/if}
</svelte:boundary>
</section>
</main>The onerror handler receives an event with a detail property containing the error. You can log it, send it to your error tracking service, or display a user-friendly message. The key: errors in child components don't propagate up and crash the boundary's siblings.
Common mistakes
- Forgetting reactivity in meta tags. If you set a meta description once and never update it when props change, you'll send stale metadata to social networks. Always ensure your
svelte:headcontent depends on reactive variables or props. - Not cleaning up global listeners. Svelte's event binding handles unsubscription, but if you manually add listeners in
onMount, you must remove them inonDestroy. Usesvelte:windowinstead to avoid this footgun. - Setting conflicting head tags from multiple components. If two sibling components both set a page title in their
svelte:head, the last one to mount wins—which might not be what you expect. Centralize critical metadata in a single parent component or layout. - Overusing
svelte:boundaryas a band-aid. Catching errors at the boundary level is good for resilience, but it's not a substitute for proper error handling. Fix the root causes in your components; use boundaries as a safety net, not a crutch. - Accessing window in SSR context. Code inside
svelte:windowhandlers runs client-side only, which is correct. But if you referencewindowdirectly outside a handler, you'll break SSR. Use thebrowserflag from$app/environmentwhen in doubt.
Key takeaways
- Use
svelte:headfor metadata that should be indexed or shared; it works reactively and integrates seamlessly with SSR. - Reach for
svelte:windowandsvelte:bodywhen you need global event handlers; Svelte's binding syntax handles cleanup automatically. - Leverage
svelte:documentfor document-level concerns like full-page scroll tracking or class management. - Deploy
svelte:boundarystrategically to isolate component failures and keep the rest of your page functional. - Centralize repeated patterns (like social metadata) in reusable wrapper components to avoid duplication and inconsistency.
- Always verify that head tags and window listeners update reactively when data changes; test with SSR enabled.






Comments
Loading comments…