Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 082 / 100

Special elements in Svelte: patterns, pitfalls and fixes

Special elements explained: the head element for meta tags, the window element for global events and boundaries for catching errors.

Expona Admin7 min read

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:

typescript
<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:

typescript
<!-- 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:

typescript
<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:

typescript
<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:

typescript
<!-- 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:

typescript
<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:

typescript
<!-- 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:head content 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 in onDestroy. Use svelte:window instead 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:boundary as 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:window handlers runs client-side only, which is correct. But if you reference window directly outside a handler, you'll break SSR. Use the browser flag from $app/environment when in doubt.

Key takeaways

  • Use svelte:head for metadata that should be indexed or shared; it works reactively and integrates seamlessly with SSR.
  • Reach for svelte:window and svelte:body when you need global event handlers; Svelte's binding syntax handles cleanup automatically.
  • Leverage svelte:document for document-level concerns like full-page scroll tracking or class management.
  • Deploy svelte:boundary strategically 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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles