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:
setContextmust be called synchronously during the component's script execution. Calling it inside an event handler,onMount, orsetTimeoutthrows an error. - Expecting context to be reactive by default: A plain value passed to
setContextis 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
setContextprovides a value to all descendant components.getContextretrieves 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
$statefor 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
setContextduring component initialization, not in callbacks or lifecycle hooks.