Script Modules: Beyond Component Instances
Every Svelte component you’ve written so far has an instance script—the <script> block that runs for each component instance. But what if you need code that runs once for the entire component type, not per instance? What if you want to share state or utilities across all instances of a component?
Enter script modules—Svelte’s mechanism for component-level code sharing. In Svelte 5, the syntax evolved from <script context="module"> to the cleaner <script module>, but the concept remains powerful: code that executes once when the component module loads, shared across all instances.
This tutorial explores script modules comprehensively: their syntax, use cases, limitations, and how they integrate with Svelte 5’s reactive system.
Chapter 1: Understanding Script Modules
1.1 Instance Scripts vs. Module Scripts
Instance Scripts (<script>):
- Run once per component instance
- Have access to props, state, and lifecycle
- Can be reactive with runes like
$state,$derived,$effect
Module Scripts (<script module>):
- Run once when the component module is first loaded
- Shared across all instances of the component
- Cannot access instance-specific data (props, state)
- Execute in module scope, not component scope
<!-- Component.svelte -->
<script module>
// This runs once for the entire component type
console.log('Component module loaded')
// Shared across all instances
let instanceCount = 0
export function getInstanceCount() {
return instanceCount
}
</script>
<script>
// This runs for each component instance
console.log('New instance created')
// Access module-level function
const count = getInstanceCount()
</script> 1.2 The Execution Model
Module scripts execute during the module loading phase, before any component instances are created:
<!-- Logger.svelte -->
<script module>
// ✅ Runs immediately when module loads
console.log('Logger module initialized')
// Module-level state
let logHistory = []
export function log(message) {
logHistory.push({ message, timestamp: Date.now() })
console.log(`[${new Date().toISOString()}] ${message}`)
}
export function getLogHistory() {
return [...logHistory]
}
</script>
<script>
let { componentName } = $props()
// Use module function
log(`${componentName} component mounted`)
</script> 1.3 Access Rules and SSR Lifecycle
- Module → Instance: Declarations in the module script are available to the instance script and markup because they share the same compiled module scope. They are not reactive to instance state—treat them like regular module-level variables.
- Instance → Module: Module scripts cannot see instance-only values (props, runes) because those are created per instance.
- Server considerations: In SvelteKit SSR, module state is shared across requests unless you explicitly isolate it (e.g., by creating fresh state inside a request-scoped module). Avoid storing per-request data in module scope.
- Environment guards: Module scripts run wherever the component module loads. Use
import { browser } from '$app/environment'(SvelteKit) or feature checks before touchingwindow/document.
Chapter 2: Common Use Cases for Script Modules
2.1 Shared Utilities and Constants
Module scripts excel at providing utilities that don’t need per-instance state:
<!-- DateUtils.svelte -->
<script module>
// Constants shared across instances
export const DATE_FORMATS = {
SHORT: 'MM/dd/yyyy',
LONG: 'MMMM dd, yyyy',
ISO: 'yyyy-MM-dd'
}
// Utility functions
export function formatDate(date, format = DATE_FORMATS.SHORT) {
// Implementation...
return new Intl.DateTimeFormat('en-US').format(date)
}
export function isWeekend(date) {
const day = date.getDay()
return day === 0 || day === 6
}
export function addDays(date, days) {
const result = new Date(date)
result.setDate(result.getDate() + days)
return result
}
</script>
<script>
let { date } = $props()
// Use shared utilities
$: formatted = formatDate(date)
$: isWeekendDay = isWeekend(date)
</script> 2.2 Instance Tracking and Analytics
Track component usage across your application:
<!-- Button.svelte -->
<script module>
let clickCount = 0
let instances = new Set()
export function recordClick(componentId) {
clickCount++
console.log(`Button clicked (total: ${clickCount})`)
}
export function registerInstance(id) {
instances.add(id)
}
export function unregisterInstance(id) {
instances.delete(id)
}
export function getStats() {
return {
totalClicks: clickCount,
activeInstances: instances.size
}
}
</script>
<script>
let { id, children, ...props } = $props()
// Register this instance
$effect(() => {
registerInstance(id)
return () => unregisterInstance(id)
})
function handleClick() {
recordClick(id)
// ... other click logic
}
</script>
<button onclick={handleClick} {...props}>
{@render children?.()}
</button> 2.3 Global Component State Management
For component-specific global state that doesn’t belong in a store:
<!-- ThemeProvider.svelte -->
<script module>
// Global theme state for all ThemeProvider instances
let currentTheme = 'light'
let subscribers = new Set()
export function setTheme(theme) {
currentTheme = theme
// Notify all subscribers
subscribers.forEach((callback) => callback(theme))
}
export function getTheme() {
return currentTheme
}
export function subscribeToTheme(callback) {
subscribers.add(callback)
return () => subscribers.delete(callback)
}
</script>
<script>
let { children } = $props()
// Subscribe to theme changes
$effect(() => {
const unsubscribe = subscribeToTheme((theme) => {
// Update component state
document.documentElement.setAttribute('data-theme', theme)
})
return unsubscribe
})
</script>
<div class="theme-provider">
{@render children?.()}
</div> 2.4 Component Registry Pattern
Create discoverable component systems:
<!-- ComponentRegistry.svelte -->
<script module>
const registry = new Map()
export function registerComponent(name, component) {
registry.set(name, component)
console.log(`Registered component: ${name}`)
}
export function getComponent(name) {
return registry.get(name)
}
export function getAllComponents() {
return Array.from(registry.entries())
}
export function unregisterComponent(name) {
return registry.delete(name)
}
</script>
<!-- Usage in other components -->
<script>
import ComponentRegistry from './ComponentRegistry.svelte'
// Register this component
$effect(() => {
ComponentRegistry.registerComponent('MyComponent', MyComponent)
return () => ComponentRegistry.unregisterComponent('MyComponent')
})
</script> Chapter 3: Exporting Snippets from Module Scripts
Svelte 5.5.0 introduced the ability to export snippets from module scripts, enabling powerful component composition patterns.
3.1 Basic Snippet Export
<!-- FormSnippets.svelte -->
<script module>
export { inputField, selectField, checkboxField }
</script>
{#snippet inputField(label, value, type = 'text', ...attrs)}
<div class="form-field">
<label>{label}</label>
<input {type} bind:value {...attrs} />
</div>
{/snippet}
{#snippet selectField(label, value, options, ...attrs)}
<div class="form-field">
<label>{label}</label>
<select bind:value {...attrs}>
{#each options as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
{/snippet}
{#snippet checkboxField(label, checked, ...attrs)}
<div class="form-field">
<label>
<input type="checkbox" bind:checked {...attrs} />
{label}
</label>
</div>
{/snippet} 3.2 Using Exported Snippets
<!-- ContactForm.svelte -->
<script>
import { inputField, selectField, checkboxField } from './FormSnippets.svelte'
let formData = $state({
name: '',
email: '',
country: '',
newsletter: false
})
const countries = [
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'uk', label: 'United Kingdom' }
]
</script>
<form>
{@render inputField('Name', formData.name, 'text', { required: true })}
{@render inputField('Email', formData.email, 'email', { required: true })}
{@render selectField('Country', formData.country, countries)}
{@render checkboxField('Subscribe to newsletter', formData.newsletter)}
</form> 3.3 Limitations of Exported Snippets
Exported snippets have strict scoping rules:
<!-- ❌ This will NOT work -->
<script>
let instanceVar = "can't access this";
</script>
<script module>
export { brokenSnippet };
</script>
{#snippet brokenSnippet()}
<!-- Error: Cannot access instance variables -->
<p>{instanceVar}</p>
{/snippet}
<!-- ✅ This WILL work -->
<script module>
const MODULE_CONSTANT = "this is fine";
export { workingSnippet };
</script>
{#snippet workingSnippet()}
<p>{MODULE_CONSTANT}</p>
{/snippet} Chapter 4: Advanced Patterns and Best Practices
4.1 Module-Level Reactive State
While module scripts can’t use runes directly, you can create reactive patterns:
<!-- ReactiveModule.svelte -->
<script module>
// Module-level reactive state using custom events
let _value = 'initial'
const listeners = new Set()
export function getValue() {
return _value
}
export function setValue(newValue) {
_value = newValue
// Notify listeners
listeners.forEach((listener) => listener(newValue))
}
export function subscribe(listener) {
listeners.add(listener)
return () => listeners.delete(listener)
}
</script>
<script>
let localValue = $state('')
// Subscribe to module changes
$effect(() => {
const unsubscribe = subscribe((newValue) => {
localValue = newValue
})
return unsubscribe
})
</script> 4.2 Combining Module Scripts with Stores
For complex shared state, combine module scripts with Svelte stores:
<!-- UserPreferences.svelte -->
<script module>
import { writable } from 'svelte/store'
// Module-level store
export const userPreferences = writable({
theme: 'light',
language: 'en',
notifications: true
})
// Utility functions
export function updatePreference(key, value) {
userPreferences.update((prefs) => ({ ...prefs, [key]: value }))
}
export function resetPreferences() {
userPreferences.set({
theme: 'light',
language: 'en',
notifications: true
})
}
</script>
<script>
import { userPreferences } from './UserPreferences.svelte'
// Use in component
let prefs = $state()
userPreferences.subscribe((value) => (prefs = value))
</script> 4.3 Testing Module Scripts
Module scripts can be challenging to test due to their shared nature:
// ModuleScript.test.js
import { getInstanceCount, recordClick } from './Button.svelte'
// Reset module state between tests
beforeEach(() => {
// If your module exposes reset functions, use them
// Otherwise, you may need to re-import or use a different strategy
})
// Test module functions
test('tracks clicks across instances', () => {
expect(getInstanceCount()).toBe(0)
recordClick('test-id')
expect(getInstanceCount()).toBe(1)
}) Chapter 5: Migration from Svelte 4
5.1 Syntax Changes
<!-- Svelte 4 -->
<script context="module">
export const sharedValue = 'hello';
</script>
<!-- Svelte 5 -->
<script module>
export const sharedValue = 'hello';
</script> 5.2 Behavioral Changes
- Module scripts now run in strict mode
- Better tree-shaking support
- Improved error messages
- Snippet export capability (new in 5.5.0)
Conclusion: When to Use Module Scripts
Module scripts are powerful but should be used judiciously:
Use module scripts for:
- Shared utilities and constants
- Component analytics and tracking
- Global component state
- Snippet libraries
- Component registries
Don’t use module scripts for:
- Instance-specific logic
- Reactive state that varies per component
- Side effects that should run per instance
Mastering module scripts unlocks new patterns for component communication and code organization in Svelte 5. They bridge the gap between individual component instances and application-level concerns, making your components more composable and maintainable.