Published 12/5/2025 · 6 min read
Tags: svelte , context , state-management
Lesson 12: Context API
Props pass data explicitly. Stores share data globally. Context sits between — sharing data through a component tree without drilling props through every level.
The Problem Context Solves
Imagine a form with nested components:
Form
└── FieldGroup
└── Field
└── Input
└── ErrorMessage
If Form manages validation state, you’d need to pass it through every component. Tedious and fragile.
Context lets Form provide data that any descendant can access directly.
Setting Context
Use setContext in a parent component:
<!-- Form.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
const errors = writable({})
const values = writable({})
setContext('form', {
errors,
values,
register: (name, validators) => { /* ... */ },
validate: () => { /* ... */ }
})
</script>
<form>
<slot />
</form>
setContext takes a key (any value, often a string or symbol) and a value to share.
Getting Context
Use getContext in any descendant:
<!-- ErrorMessage.svelte -->
<script>
import { getContext } from 'svelte'
export let name
const { errors } = getContext('form')
$: error = $errors[name]
</script>
{#if error}
<span class="error">{error}</span>
{/if}
The child doesn’t need to know how many levels up Form is.
Context Keys
String keys work but can collide. For library code, use symbols:
// context-keys.js
export const FORM_KEY = Symbol("form");
export const THEME_KEY = Symbol("theme");
<script>
import { setContext } from 'svelte'
import { FORM_KEY } from './context-keys.js'
setContext(FORM_KEY, { /* ... */ })
</script>
Symbols guarantee uniqueness.
Context vs Stores
When to use which?
Use stores when:
- State is truly global (theme, auth, cart)
- Unrelated components need the same data
- State should persist across navigation
Use context when:
- State is scoped to a component tree
- Multiple instances might exist (multiple forms on a page)
- You’re building a component library
A form’s validation state shouldn’t be global. Multiple forms could exist. Use context.
User authentication affects the whole app. Use a store.
Context with Stores
The best pattern: put stores in context.
<!-- Tabs.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
export let initial = 0
const activeTab = writable(initial)
setContext('tabs', {
activeTab,
registerTab: () => { /* ... */ }
})
</script>
<div class="tabs">
<slot />
</div>
<!-- Tab.svelte -->
<script>
import { getContext } from 'svelte'
export let id
const { activeTab } = getContext('tabs')
$: isActive = $activeTab === id
</script>
<button
class:active={isActive}
onclick={() => $activeTab = id}
>
<slot />
</button>
The store inside context gets reactivity. The context scopes it to this tab group.
hasContext
Check if context exists:
<script>
import { hasContext, getContext } from 'svelte'
const hasForm = hasContext('form')
// Only try to get context if it exists
const form = hasForm ? getContext('form') : null
</script>
Useful for optional integration with parent components.
Comparing to Vue
Vue’s provide/inject:
<!-- Parent.vue -->
<script setup>
import { provide, ref } from "vue";
const theme = ref("light");
provide("theme", theme);
</script>
<!-- Child.vue -->
<script setup>
import { inject } from "vue";
const theme = inject("theme");
</script>
Svelte:
<!-- Parent.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
const theme = writable('light')
setContext('theme', theme)
</script>
<!-- Child.svelte -->
<script>
import { getContext } from 'svelte'
const theme = getContext('theme')
</script>
<p>Theme: {$theme}</p>
Similar concepts. Vue’s ref is reactive by default. Svelte needs a store for reactivity in context.
Practical Example: Toast System
<!-- ToastProvider.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
const toasts = writable([])
let id = 0
function addToast(message, type = 'info', duration = 3000) {
const toast = { id: id++, message, type }
toasts.update(t => [...t, toast])
setTimeout(() => {
removeToast(toast.id)
}, duration)
}
function removeToast(id) {
toasts.update(t => t.filter(toast => toast.id !== id))
}
setContext('toasts', {
add: addToast,
remove: removeToast,
toasts
})
</script>
<slot />
<div class="toast-container">
{#each $toasts as toast (toast.id)}
<div class="toast {toast.type}">
<p>{toast.message}</p>
<button onclick={() => removeToast(toast.id)}>×</button>
</div>
{/each}
</div>
<style>
.toast-container {
position: fixed;
top: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.toast {
padding: 1rem;
border-radius: 4px;
background: #333;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
min-width: 200px;
}
.toast.success { background: #16a34a; }
.toast.error { background: #dc2626; }
.toast.warning { background: #d97706; }
</style>
Usage anywhere in the app:
<script>
import { getContext } from 'svelte'
const { add: addToast } = getContext('toasts')
async function handleSubmit() {
try {
await saveData()
addToast('Saved successfully!', 'success')
} catch (e) {
addToast('Failed to save', 'error')
}
}
</script>
Practical Example: Form Validation
<!-- FormProvider.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
const values = writable({})
const errors = writable({})
const touched = writable({})
const validators = {}
function register(name, validate = () => null) {
validators[name] = validate
values.update(v => ({ ...v, [name]: '' }))
}
function setValue(name, value) {
values.update(v => ({ ...v, [name]: value }))
touched.update(t => ({ ...t, [name]: true }))
validateField(name, value)
}
function validateField(name, value) {
const error = validators[name]?.(value)
errors.update(e => ({ ...e, [name]: error }))
return !error
}
function validateAll() {
let valid = true
const currentValues = {}
values.subscribe(v => Object.assign(currentValues, v))()
for (const [name, value] of Object.entries(currentValues)) {
if (!validateField(name, value)) {
valid = false
}
}
return valid
}
setContext('form', {
values,
errors,
touched,
register,
setValue,
validateAll
})
</script>
<slot />
<!-- FormField.svelte -->
<script>
import { getContext, onMount } from 'svelte'
export let name
export let label = name
export let type = 'text'
export let required = false
const { values, errors, touched, register, setValue } = getContext('form')
onMount(() => {
register(name, (value) => {
if (required && !value) return `${label} is required`
return null
})
})
$: value = $values[name] || ''
$: error = $errors[name]
$: showError = $touched[name] && error
</script>
<div class="field">
<label for={name}>{label}</label>
<input
{type}
id={name}
{value}
oninput={(e) => setValue(name, e.target.value)}
/>
{#if showError}
<span class="error">{error}</span>
{/if}
</div>
Key Takeaways
setContext(key, value)shares data with descendantsgetContext(key)retrieves that data- Use symbols for keys in libraries to avoid collisions
- Put stores in context for reactivity
- Context is scoped; stores are global
- Use
hasContextto check before getting - Perfect for component libraries (tabs, forms, modals)
Related Articles
- x402 with SvelteKit: Full-Stack Example
Build a complete SvelteKit application with x402 payments - wallet connection, protected routes, and automatic payment handling.
- Interacting with Programs from Svelte
Build Svelte components that interact with Solana programs - token balances, transfers, and real-time updates.
- Signing Messages and Transactions in the Browser
Learn to sign messages for authentication and build transactions that users approve through their wallet.