3 min read
On this page

Context

Context is Svelte's mechanism for passing data down the component tree without threading props through every intermediate component. It works like dependency injection: a parent component provides a value, and any descendant can retrieve it. Context is per-component-tree, not global, which makes it the right tool for sharing data within a specific subtree of your application.

setContext & getContext

A parent component sets a context value. Any descendant retrieves it:

<!-- ThemeProvider.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';

  interface Theme {
    primary: string;
    secondary: string;
    background: string;
    text: string;
  }

  let { theme, children }: { theme: Theme; children: any } = $props();

  setContext('theme', theme);
</script>

{@render children()}
<!-- ThemedButton.svelte (any depth below ThemeProvider) -->
<script lang="ts">
  import { getContext } from 'svelte';

  interface Theme {
    primary: string;
    secondary: string;
    background: string;
    text: string;
  }

  const theme = getContext<Theme>('theme');
</script>

<button style="background: {theme.primary}; color: white;">
  <slot />
</button>
<!-- App.svelte -->
<script>
  import ThemeProvider from './ThemeProvider.svelte';
  import ThemedButton from './ThemedButton.svelte';
</script>

<ThemeProvider theme={{ primary: '#0066cc', secondary: '#444', background: '#fff', text: '#333' }}>
  <div>
    <h1>My App</h1>
    <!-- ThemedButton can be nested at any depth -->
    <ThemedButton>Click me</ThemedButton>
  </div>
</ThemeProvider>

How Context Works

setContext must be called during component initialization (in the <script> block, not inside an event handler or timeout). It associates a key with a value for the current component instance. getContext retrieves the value by walking up the component tree until it finds a matching key.

The key can be any value, but strings are most common. For libraries, use a unique symbol to avoid collisions:

// src/lib/context-keys.ts
export const THEME_KEY = Symbol('theme');
export const AUTH_KEY = Symbol('auth');
<script>
  import { setContext } from 'svelte';
  import { THEME_KEY } from '$lib/context-keys';

  setContext(THEME_KEY, { primary: '#0066cc' });
</script>

Context vs Stores

This distinction matters and is a frequent source of confusion:

Stores are global. A store defined in a module is shared across the entire application. Every component that imports the store sees the same value.

Context is per-component-tree. Each instance of a provider component creates its own context. Two separate ThemeProvider instances can provide different themes to their respective subtrees.

<!-- Two independent context trees -->
<ThemeProvider theme={{ primary: 'blue' }}>
  <Sidebar />
  <!-- Everything here sees blue -->
</ThemeProvider>

<ThemeProvider theme={{ primary: 'red' }}>
  <MainContent />
  <!-- Everything here sees red -->
</ThemeProvider>

With a store, this would be impossible. There is only one store value, shared everywhere.

Aspect Context Stores
Scope Per component tree Global
Multiple instances Yes No (one value)
Reactive Only if the value is reactive (store or $state) Always
Access Only descendants Any component
SSR safe Yes Can cause issues with shared state

Reactive Context

Context values are not reactive by default. If you pass a plain object, consumers get a snapshot. To make context reactive, pass a store or use $state in a .svelte.ts module:

With a Store

<!-- AuthProvider.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';

  interface User {
    name: string;
    role: string;
  }

  const user = writable<User | null>(null);

  setContext('auth', {
    user,
    login: async (email: string, password: string) => {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
        headers: { 'Content-Type': 'application/json' }
      });
      const data = await response.json();
      user.set(data.user);
    },
    logout: () => {
      user.set(null);
    }
  });
</script>

<slot />
<!-- UserMenu.svelte -->
<script>
  import { getContext } from 'svelte';
  const { user, logout } = getContext('auth');
</script>

{#if $user}
  <p>Logged in as {$user.name}</p>
  <button onclick={logout}>Logout</button>
{:else}
  <a href="/login">Login</a>
{/if}

With $state (Svelte 5)

You can pass reactive state through context by using an object with $state properties:

<!-- LocaleProvider.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';

  let locale = $state('en');
  let translations = $state({});

  setContext('locale', {
    get locale() { return locale; },
    set locale(v: string) { locale = v; },
    get translations() { return translations; },
    loadTranslations: async (lang: string) => {
      const response = await fetch(`/i18n/${lang}.json`);
      translations = await response.json();
      locale = lang;
    }
  });
</script>

<slot />

Note the getter/setter pattern. Passing { locale } directly would capture the value, not the reactive binding. Getters ensure consumers always read the current value.

Real-World Context Patterns

Authentication

<!-- src/routes/+layout.svelte -->
<script>
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';

  let { data, children } = $props();
  const user = writable(data.user);

  setContext('auth', { user });
</script>

{@render children()}
<!-- Any deeply nested component -->
<script>
  import { getContext } from 'svelte';
  const { user } = getContext('auth');
</script>

{#if $user?.role === 'admin'}
  <AdminPanel />
{/if}

Notification System

<!-- NotificationProvider.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';

  interface Notification {
    id: number;
    message: string;
    type: 'info' | 'error' | 'success';
  }

  let notifications = $state<Notification[]>([]);
  let nextId = 0;

  setContext('notifications', {
    get list() { return notifications; },
    add: (message: string, type: 'info' | 'error' | 'success' = 'info') => {
      const id = nextId++;
      notifications.push({ id, message, type });
      setTimeout(() => {
        const index = notifications.findIndex(n => n.id === id);
        if (index !== -1) notifications.splice(index, 1);
      }, 5000);
    },
    dismiss: (id: number) => {
      const index = notifications.findIndex(n => n.id === id);
      if (index !== -1) notifications.splice(index, 1);
    }
  });
</script>

<slot />

<div class="notification-container">
  {#each notifications as notification (notification.id)}
    <div class="notification notification-{notification.type}">
      {notification.message}
    </div>
  {/each}
</div>
<!-- Any descendant component -->
<script>
  import { getContext } from 'svelte';
  const notifications = getContext('notifications');

  async function save() {
    try {
      await saveData();
      notifications.add('Saved successfully!', 'success');
    } catch {
      notifications.add('Failed to save.', 'error');
    }
  }
</script>

Form Validation

<!-- Form.svelte -->
<script>
  import { setContext } from 'svelte';

  let errors = $state({});

  setContext('form', {
    get errors() { return errors; },
    setError: (field, message) => { errors[field] = message; },
    clearError: (field) => { delete errors[field]; },
    clearAll: () => { errors = {}; }
  });
</script>

<form>
  <slot />
</form>
<!-- FormField.svelte -->
<script>
  import { getContext } from 'svelte';
  let { name, label, children } = $props();
  const form = getContext('form');
  let error = $derived(form.errors[name]);
</script>

<div class="field" class:has-error={error}>
  <label>{label}</label>
  {@render children()}
  {#if error}
    <span class="error">{error}</span>
  {/if}
</div>

Common Pitfalls

  • Calling setContext outside component initialization: setContext must be called synchronously during the component's script execution. Calling it inside an event handler, onMount, or setTimeout throws an error.
  • Expecting context to be reactive by default: A plain value passed to setContext is not reactive. If the value changes, consumers will not see the update. Wrap in a store or use getters with $state.
  • Using context for truly global state: Context is scoped to a component subtree. If you need state accessible from anywhere (including utility functions and API modules), use a store instead.
  • String key collisions: Two libraries using setContext('theme', ...) will collide. Use symbols for library code and unique strings for application code.
  • Deep nesting without context: If you are passing the same prop through three or more levels of components, that is a sign you should use context. Do not wait until five levels of prop drilling to introduce it.

Key Takeaways

  • setContext provides a value to all descendant components. getContext retrieves it. No prop drilling required.
  • Context is per-component-tree, not global. Different subtrees can have different context values.
  • Context values are not reactive by default. Use stores or getter patterns with $state for reactive context.
  • Common use cases: authentication, theming, localization, form state, notification systems.
  • Use context when data needs to reach deeply nested components within a specific tree. Use stores when state must be truly global.
  • Call setContext during component initialization, not in callbacks or lifecycle hooks.