Scoped Constants for Cleaner Template Logic
Among Svelte 5’s template directives, @const might appear to be the most straightforward—a simple mechanism for declaring local constants within template blocks. Yet this apparent simplicity belies a powerful tool that, when wielded effectively, can dramatically improve code readability, reduce repetitive calculations, and create cleaner separation between data transformation and presentation logic.
The @const directive addresses a fundamental tension in component development: the need to perform calculations or transformations on data at the point of use, without cluttering the component’s script section with values that are only relevant within a specific template context. Understanding when and how to leverage this directive will elevate your Svelte templates from functional to elegant.
The Problem
Computation at the Point of Consumption
Consider a common scenario in component development. You’re iterating over a collection of items, and for each item, you need to compute several derived values that are only relevant within that iteration context. Without @const, you face an uncomfortable choice:
1: Inline Everything
<script>
let products = $state([
{ name: 'Widget', price: 29.99, quantity: 3, taxRate: 0.08 },
{ name: 'Gadget', price: 49.99, quantity: 1, taxRate: 0.08 },
{ name: 'Gizmo', price: 19.99, quantity: 5, taxRate: 0.05 }
])
</script>
{#each products as product}
<div class="product-row">
<span>{product.name}</span>
<span>{product.quantity} × ${product.price.toFixed(2)}</span>
<span>Subtotal: ${(product.price * product.quantity).toFixed(2)}</span>
<span>Tax: ${(product.price * product.quantity * product.taxRate).toFixed(2)}</span>
<span>
Total: ${(product.price * product.quantity * (1 + product.taxRate)).toFixed(2)}
</span>
</div>
{/each} This approach suffers from multiple issues: repeated calculations, poor readability, and the risk of inconsistencies if you update one formula but forget another.
2: Pre-compute in Script
<script>
let products = $state([
{ name: 'Widget', price: 29.99, quantity: 3, taxRate: 0.08 },
{ name: 'Gadget', price: 49.99, quantity: 1, taxRate: 0.08 },
{ name: 'Gizmo', price: 19.99, quantity: 5, taxRate: 0.05 }
])
let enrichedProducts = $derived(
products.map((product) => ({
...product,
subtotal: product.price * product.quantity,
tax: product.price * product.quantity * product.taxRate,
total: product.price * product.quantity * (1 + product.taxRate)
}))
)
</script>
{#each enrichedProducts as product}
<div class="product-row">
<span>{product.name}</span>
<span>{product.quantity} × ${product.price.toFixed(2)}</span>
<span>Subtotal: ${product.subtotal.toFixed(2)}</span>
<span>Tax: ${product.tax.toFixed(2)}</span>
<span>Total: ${product.total.toFixed(2)}</span>
</div>
{/each} Better, but now your script section contains transformation logic that’s only relevant to this specific template rendering. As components grow, this pattern leads to bloated script sections full of derived values that exist solely to serve template display needs.
3: The @const Solution
<script>
let products = $state([
{ name: 'Widget', price: 29.99, quantity: 3, taxRate: 0.08 },
{ name: 'Gadget', price: 49.99, quantity: 1, taxRate: 0.08 },
{ name: 'Gizmo', price: 19.99, quantity: 5, taxRate: 0.05 }
])
</script>
{#each products as product}
{@const subtotal = product.price * product.quantity}
{@const tax = subtotal * product.taxRate}
{@const total = subtotal + tax}
<div class="product-row">
<span>{product.name}</span>
<span>{product.quantity} × ${product.price.toFixed(2)}</span>
<span>Subtotal: ${subtotal.toFixed(2)}</span>
<span>Tax: ${tax.toFixed(2)}</span>
<span>Total: ${total.toFixed(2)}</span>
</div>
{/each} Now the computation lives exactly where it’s relevant, calculated once per iteration, and the values build upon each other naturally. The template is clean, the script focuses on core state, and the logic flow is immediately apparent.
Syntax and Semantics
The Complete Picture
The @const directive follows a straightforward syntax:
{@const name = expression} The name must be a valid JavaScript identifier (or destructuring pattern), and expression can be any JavaScript expression. The resulting constant is scoped to the block in which it’s declared—it’s not accessible outside that block, and it cannot be reassigned.
Placement Rules
the directive @const can only appear as an immediate child of:
- Block statements:
{#if ...},{#each ...},{#await ...},{#key ...} - Snippets:
{#snippet ...} - Components:
<Component>...</Component> - Special elements:
<svelte:boundary>
It cannot be used at the top level of a component template or inside raw HTML elements. This restriction is intentional—top-level constants belong in the <script> section, and {@const ...} is specifically designed for scoped, context-dependent computations.
<!-- Valid placements -->
{#if condition}
{@const value = computeSomething()}
<p>{value}</p>
{/if}
{#each items as item}
{@const processed = transform(item)}
<div>{processed}</div>
{/each}
{#snippet card(data)}
{@const formattedDate = formatDate(data.created)}
<article>
<time>{formattedDate}</time>
</article>
{/snippet}
<Modal>
{@const modalId = generateId()}
<div id={modalId}>...</div>
</Modal>
<!-- Invalid: top-level -->
{@const topLevel = 'error'}
<!-- Compiler error! -->
<p>{topLevel}</p>
<!-- Invalid: inside HTML element -->
<div>
{@const nested = 'error'}
<!-- Compiler error! -->
</div> Multiple Constants and Dependencies
You can declare multiple @const directives within a single block, and later constants can reference earlier ones:
{#each orders as order}
{@const itemCount = order.items.length}
{@const subtotal = order.items.reduce((sum, item) => sum + item.price, 0)}
{@const shipping = itemCount > 5 ? 0 : 9.99}
{@const discount = subtotal > 100 ? subtotal * 0.1 : 0}
{@const total = subtotal + shipping - discount}
<div class="order-summary">
<p>Items: {itemCount}</p>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
{#if shipping === 0}
<p class="free-shipping">Free shipping!</p>
{:else}
<p>Shipping: ${shipping.toFixed(2)}</p>
{/if}
{#if discount > 0}
<p class="discount">Discount: -${discount.toFixed(2)}</p>
{/if}
<p class="total">Total: ${total.toFixed(2)}</p>
</div>
{/each} This chained dependency pattern is one of @const’s greatest strengths—it allows you to build up complex calculations step by step, with each intermediate value clearly named and available for both display and further computation.
Destructuring
Elegant Data Extraction
The @const directive fully supports JavaScript destructuring patterns, enabling elegant extraction of nested data:
Object Destructuring
<script>
let users = $state([
{
id: 1,
profile: {
name: 'Alice Chen',
avatar: '/avatars/alice.jpg',
bio: 'Software engineer and coffee enthusiast'
},
stats: { posts: 42, followers: 1337, following: 256 }
},
{
id: 2,
profile: {
name: 'Bob Smith',
avatar: '/avatars/bob.jpg',
bio: 'Designer by day, gamer by night'
},
stats: { posts: 18, followers: 892, following: 143 }
}
])
</script>
{#each users as user (user.id)}
{@const { name, avatar, bio } = user.profile}
{@const { posts, followers, following } = user.stats}
{@const engagementRatio = followers / (following || 1)}
<article class="user-card">
<img src={avatar} alt="{name}'s avatar" />
<h2>{name}</h2>
<p class="bio">{bio}</p>
<div class="stats">
<span>{posts} posts</span>
<span>{followers} followers</span>
<span>{following} following</span>
</div>
<div class="engagement" class:high={engagementRatio > 5}>
Engagement ratio: {engagementRatio.toFixed(2)}
</div>
</article>
{/each} Array Destructuring
<script>
let coordinates = $state([
[0, 0, 'Origin'],
[10, 20, 'Point A'],
[30, 40, 'Point B'],
[-5, 15, 'Point C']
])
function calculateDistance(x, y) {
return Math.sqrt(x * x + y * y)
}
function getQuadrant(x, y) {
if (x >= 0 && y >= 0) return 'I'
if (x < 0 && y >= 0) return 'II'
if (x < 0 && y < 0) return 'III'
return 'IV'
}
</script>
{#each coordinates as coord, index}
{@const [x, y, label] = coord}
{@const distance = calculateDistance(x, y)}
{@const quadrant = getQuadrant(x, y)}
<div class="coordinate-row">
<span class="index">{index + 1}.</span>
<span class="label">{label}</span>
<span class="coords">({x}, {y})</span>
<span class="distance">{distance.toFixed(2)} units from origin</span>
<span class="quadrant">Quadrant {quadrant}</span>
</div>
{/each} Combined Patterns with Defaults
<script>
let apiResponses = $state([
{ data: { user: { name: 'Alice' }, metadata: { version: 2 } } },
{ data: { user: {}, metadata: {} } },
{ data: null },
{ error: 'Network timeout' }
])
</script>
{#each apiResponses as response, i}
{@const {
data: { user: { name = 'Unknown' } = {}, metadata: { version = 1 } = {} } = {},
error = null
} = response}
<div class="response-item" class:error>
<span>Response {i + 1}:</span>
{#if error}
<span class="error-message">Error: {error}</span>
{:else}
<span>User: {name} (API v{version})</span>
{/if}
</div>
{/each} Integration with Svelte 5 Runes
While @const creates non-reactive constants, it integrates seamlessly with Svelte 5’s reactivity system. The constants are recalculated whenever the template block re-renders due to reactive state changes.
Reactive Data, Constant Computations
<script>
let multiplier = $state(1)
let items = $state([
{ name: 'Item A', baseValue: 10 },
{ name: 'Item B', baseValue: 25 },
{ name: 'Item C', baseValue: 15 }
])
</script>
<input type="range" bind:value={multiplier} min="1" max="10" />
<p>Multiplier: {multiplier}x</p>
{#each items as item}
{@const scaledValue = item.baseValue * multiplier}
{@const rating = scaledValue > 100 ? 'High' : scaledValue > 50 ? 'Medium' : 'Low'}
<div class="item-card">
<h3>{item.name}</h3>
<p>Base: {item.baseValue}</p>
<p>Scaled: {scaledValue}</p>
<span class="rating rating-{rating.toLowerCase()}">{rating}</span>
</div>
{/each} When multiplier changes, Svelte re-renders the #each block, and all @const values are recalculated with the new multiplier. The constants themselves aren’t reactive—they don’t trigger updates—but they participate in the reactive rendering cycle.
Combining with $derived for Complex Scenarios
For scenarios where you need both component-level derived state and block-scoped constants:
<script>
let searchQuery = $state('')
let sortOrder = $state('name')
let products = $state([
{ id: 1, name: 'Alpha Widget', price: 29.99, category: 'widgets' },
{ id: 2, name: 'Beta Gadget', price: 49.99, category: 'gadgets' },
{ id: 3, name: 'Gamma Widget', price: 19.99, category: 'widgets' },
{ id: 4, name: 'Delta Gadget', price: 39.99, category: 'gadgets' }
])
// Component-level derived: filtering and sorting logic
let filteredProducts = $derived.by(() => {
let result = products.filter((p) => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
return result.sort((a, b) => {
if (sortOrder === 'name') return a.name.localeCompare(b.name)
if (sortOrder === 'price') return a.price - b.price
return 0
})
})
let totalValue = $derived(filteredProducts.reduce((sum, p) => sum + p.price, 0))
</script>
<input bind:value={searchQuery} placeholder="Search products..." />
<select bind:value={sortOrder}>
<option value="name">Sort by Name</option>
<option value="price">Sort by Price</option>
</select>
<p>Showing {filteredProducts.length} products (Total: ${totalValue.toFixed(2)})</p>
{#each filteredProducts as product (product.id)}
{@const priceCategory =
product.price > 40 ? 'premium' : product.price > 25 ? 'standard' : 'budget'}
{@const isWidget = product.category === 'widgets'}
{@const displayPrice = `$${product.price.toFixed(2)}`}
<div class="product-card {priceCategory}" class:widget={isWidget}>
<h3>{product.name}</h3>
<span class="price">{displayPrice}</span>
<span class="category">{product.category}</span>
<span class="tier">{priceCategory}</span>
</div>
{/each} Here, $derived handles the filtering and sorting that affects which items appear and their order—logic that belongs at the component level. Meanwhile, @const handles per-item presentation logic—categorization, formatting, and conditional styling that’s only relevant within each iteration.
Advanced Pattern
1. Snippet Parameters and Constants
When combined with snippets, @const enables powerful patterns for reusable template fragments:
<script>
let transactions = $state([
{ id: 1, type: 'deposit', amount: 500, date: '2025-01-15', status: 'completed' },
{ id: 2, type: 'withdrawal', amount: 200, date: '2025-01-14', status: 'completed' },
{ id: 3, type: 'transfer', amount: 150, date: '2025-01-13', status: 'pending' },
{ id: 4, type: 'deposit', amount: 1000, date: '2025-01-12', status: 'completed' }
])
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})
}
function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount)
}
</script>
{#snippet transactionRow(transaction)}
{@const { id, type, amount, date, status } = transaction}
{@const formattedAmount = formatCurrency(amount)}
{@const formattedDate = formatDate(date)}
{@const isCredit = type === 'deposit'}
{@const amountClass = isCredit ? 'credit' : 'debit'}
{@const statusIcon = status === 'completed' ? '✓' : '⏳'}
<tr class="transaction-row" data-status={status}>
<td class="id">#{id}</td>
<td class="type">{type}</td>
<td class="amount {amountClass}">
{isCredit ? '+' : '-'}{formattedAmount}
</td>
<td class="date">{formattedDate}</td>
<td class="status">
<span class="status-icon">{statusIcon}</span>
{status}
</td>
</tr>
{/snippet}
{#snippet transactionSummary(transactions)}
{@const totalDeposits = transactions
.filter((t) => t.type === 'deposit' && t.status === 'completed')
.reduce((sum, t) => sum + t.amount, 0)}
{@const totalWithdrawals = transactions
.filter((t) => t.type === 'withdrawal' && t.status === 'completed')
.reduce((sum, t) => sum + t.amount, 0)}
{@const netChange = totalDeposits - totalWithdrawals}
{@const pendingCount = transactions.filter((t) => t.status === 'pending').length}
<div class="summary-panel">
<div class="summary-item">
<span class="label">Total Deposits</span>
<span class="value credit">{formatCurrency(totalDeposits)}</span>
</div>
<div class="summary-item">
<span class="label">Total Withdrawals</span>
<span class="value debit">{formatCurrency(totalWithdrawals)}</span>
</div>
<div class="summary-item">
<span class="label">Net Change</span>
<span class="value" class:credit={netChange >= 0} class:debit={netChange < 0}>
{formatCurrency(Math.abs(netChange))}
{netChange >= 0 ? '↑' : '↓'}
</span>
</div>
{#if pendingCount > 0}
<div class="summary-item pending-notice">
<span>{pendingCount} pending transaction{pendingCount > 1 ? 's' : ''}</span>
</div>
{/if}
</div>
{/snippet}
<div class="transactions-container">
{@render transactionSummary(transactions)}
<table class="transactions-table">
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Amount</th>
<th>Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{#each transactions as transaction (transaction.id)}
{@render transactionRow(transaction)}
{/each}
</tbody>
</table>
</div>
<style>
.credit {
color: #16a34a;
}
.debit {
color: #dc2626;
}
.transaction-row[data-status='pending'] {
opacity: 0.7;
}
.pending-notice {
background: #fef3c7;
padding: 0.5rem;
border-radius: 4px;
}
</style> 2. Conditional Block Constants
Within {#if} and {:else if} blocks, @const allows you to compute values that are only relevant when certain conditions are met:
<script>
let userStatus = $state('premium') // 'free' | 'basic' | 'premium'
let user = $state({
name: 'Alice',
credits: 150,
subscription: {
plan: 'premium',
expiresAt: '2025-06-15',
features: ['unlimited_downloads', 'priority_support', 'early_access']
}
})
</script>
{#if userStatus === 'premium'}
{@const { plan, expiresAt, features } = user.subscription}
{@const daysRemaining = Math.ceil((new Date(expiresAt) - new Date()) / (1000 * 60 * 60 * 24))}
{@const isExpiringSoon = daysRemaining < 30}
<div class="premium-panel">
<h2>Premium Member</h2>
<p class="plan-name">{plan.charAt(0).toUpperCase() + plan.slice(1)} Plan</p>
<div class="expiry" class:warning={isExpiringSoon}>
{#if isExpiringSoon}
<span class="warning-icon">⚠️</span>
Renew soon! Only {daysRemaining} days remaining
{:else}
{daysRemaining} days remaining
{/if}
</div>
<ul class="features">
{#each features as feature}
{@const displayName = feature.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())}
<li>✓ {displayName}</li>
{/each}
</ul>
</div>
{:else if userStatus === 'basic'}
{@const upgradeDiscount = user.credits >= 100 ? 0.2 : 0.1}
{@const discountedPrice = (9.99 * (1 - upgradeDiscount)).toFixed(2)}
<div class="basic-panel">
<h2>Basic Member</h2>
<p>You have {user.credits} credits</p>
<div class="upgrade-offer">
<p>Upgrade to Premium!</p>
<p class="price">
<span class="original">$9.99</span>
<span class="discounted">${discountedPrice}</span>
<span class="savings">({(upgradeDiscount * 100).toFixed(0)}% off!)</span>
</p>
</div>
</div>
{:else}
{@const creditsToBasic = Math.max(0, 50 - user.credits)}
<div class="free-panel">
<h2>Free Member</h2>
<p>You have {user.credits} credits</p>
{#if creditsToBasic > 0}
<p class="goal">Earn {creditsToBasic} more credits to unlock Basic features!</p>
{:else}
<p class="ready">You're eligible for Basic membership!</p>
{/if}
</div>
{/if} Each branch computes only the values it needs, and those values don’t pollute other branches or the component’s script section.
3. Component Children with Constants
When content is passed to components, @const can compute values within that context:
<!-- DataGrid.svelte -->
<script>
let { data, children } = $props()
</script>
<div class="data-grid">
{#each data as row, rowIndex}
<div class="grid-row" data-row={rowIndex}>
{@render children(row, rowIndex)}
</div>
{/each}
</div> <!-- App.svelte -->
<script>
import DataGrid from './DataGrid.svelte'
let employees = $state([
{ name: 'Alice', department: 'Engineering', salary: 95000, startDate: '2020-03-15' },
{ name: 'Bob', department: 'Design', salary: 85000, startDate: '2021-07-22' },
{ name: 'Charlie', department: 'Engineering', salary: 110000, startDate: '2019-01-10' }
])
</script>
<DataGrid data={employees}>
{#snippet children(employee, index)}
{@const yearsEmployed = (
(Date.now() - new Date(employee.startDate)) /
(1000 * 60 * 60 * 24 * 365)
).toFixed(1)}
{@const seniorityLevel = yearsEmployed >= 3 ? 'Senior' : yearsEmployed >= 1 ? 'Mid' : 'Junior'}
{@const annualizedSalary = employee.salary.toLocaleString()}
{@const isHighEarner = employee.salary > 100000}
<div class="employee-cell name">{employee.name}</div>
<div class="employee-cell department">{employee.department}</div>
<div class="employee-cell salary" class:high-earner={isHighEarner}>
${annualizedSalary}
</div>
<div class="employee-cell tenure">
{yearsEmployed} years ({seniorityLevel})
</div>
{/snippet}
</DataGrid> 4. Await Block Constants
The @const directive works within all branches of #await blocks:
<script>
let userId = $state(1)
async function fetchUserData(id) {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error('Failed to fetch user')
return response.json()
}
let userPromise = $derived(fetchUserData(userId))
</script>
{#await userPromise}
{@const loadingMessage = `Loading user ${userId}...`}
<div class="loading-state">
<span class="spinner"></span>
<p>{loadingMessage}</p>
</div>
{:then user}
{@const { firstName, lastName, email, role } = user}
{@const fullName = `${firstName} ${lastName}`}
{@const isAdmin = role === 'admin'}
{@const initials = `${firstName[0]}${lastName[0]}`}
<div class="user-profile" class:admin={isAdmin}>
<div class="avatar">{initials}</div>
<h2>{fullName}</h2>
<p class="email">{email}</p>
<span class="role-badge">{role}</span>
</div>
{:catch error}
{@const errorMessage = error.message || 'Unknown error occurred'}
{@const errorCode = error.code || 'UNKNOWN'}
{@const canRetry = errorCode !== 'AUTH_FAILED'}
<div class="error-state">
<h3>Error Loading User</h3>
<p class="error-message">{errorMessage}</p>
<code class="error-code">{errorCode}</code>
{#if canRetry}
<button onclick={() => (userId = userId)}>Retry</button>
{:else}
<p>Please log in again to continue.</p>
{/if}
</div>
{/await} 5. Key Block Constants
While the #key block destroys and recreates its contents when the key changes, @const within it computes fresh values for each recreation:
<script>
import { fly } from 'svelte/transition'
let notificationId = $state(1)
let notifications = $state({
1: { type: 'success', title: 'Upload Complete', message: 'Your file has been uploaded.' },
2: { type: 'warning', title: 'Low Storage', message: 'You are running low on storage.' },
3: { type: 'error', title: 'Connection Lost', message: 'Please check your internet.' }
})
function nextNotification() {
notificationId = (notificationId % 3) + 1
}
</script>
{#key notificationId}
{@const notification = notifications[notificationId]}
{@const { type, title, message } = notification}
{@const icon = type === 'success' ? '✓' : type === 'warning' ? '⚠' : '✕'}
{@const colorClass = `notification-${type}`}
<div class="notification {colorClass}" transition:fly={{ y: -20, duration: 300 }}>
<span class="icon">{icon}</span>
<div class="content">
<h4>{title}</h4>
<p>{message}</p>
</div>
</div>
{/key}
<button onclick={nextNotification}>Next Notification</button> Real-World Example
Data Table with Computed Columns
Here’s a comprehensive example combining multiple @const patterns:
<script>
let sortColumn = $state('name')
let sortDirection = $state('asc')
let inventory = $state([
{ sku: 'WDG-001', name: 'Widget Pro', price: 29.99, cost: 12.0, stock: 150, reorderPoint: 50 },
{ sku: 'WDG-002', name: 'Widget Basic', price: 19.99, cost: 8.0, stock: 45, reorderPoint: 50 },
{
sku: 'GDG-001',
name: 'Gadget Elite',
price: 89.99,
cost: 35.0,
stock: 200,
reorderPoint: 30
},
{
sku: 'GDG-002',
name: 'Gadget Standard',
price: 59.99,
cost: 25.0,
stock: 25,
reorderPoint: 40
},
{
sku: 'ACC-001',
name: 'Accessory Pack',
price: 14.99,
cost: 5.0,
stock: 500,
reorderPoint: 100
}
])
let sortedInventory = $derived.by(() => {
return [...inventory].sort((a, b) => {
let aVal = a[sortColumn]
let bVal = b[sortColumn]
if (typeof aVal === 'string') {
return sortDirection === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal)
}
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal
})
})
// Footer aggregations - moved here since they can't be @const inside <tfoot>
let totalCostValue = $derived(
sortedInventory.reduce((sum, item) => sum + item.stock * item.cost, 0)
)
let totalPotentialRevenue = $derived(
sortedInventory.reduce((sum, item) => sum + item.stock * item.price, 0)
)
let totalItems = $derived(sortedInventory.reduce((sum, item) => sum + item.stock, 0))
let lowStockCount = $derived(
sortedInventory.filter((item) => item.stock <= item.reorderPoint).length
)
function toggleSort(column) {
if (sortColumn === column) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'
} else {
sortColumn = column
sortDirection = 'asc'
}
}
</script>
{#snippet sortableHeader(column, label)}
{@const isActive = sortColumn === column}
{@const arrow = isActive ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
<th class:active={isActive} onclick={() => toggleSort(column)}>
{label}
<span class="sort-arrow">{arrow}</span>
</th>
{/snippet}
<table class="inventory-table">
<thead>
<tr>
{@render sortableHeader('sku', 'SKU')}
{@render sortableHeader('name', 'Product')}
{@render sortableHeader('price', 'Price')}
{@render sortableHeader('cost', 'Cost')}
<th>Margin</th>
{@render sortableHeader('stock', 'Stock')}
<th>Status</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{#each sortedInventory as item (item.sku)}
{@const margin = ((item.price - item.cost) / item.price) * 100}
{@const marginClass = margin > 60 ? 'high' : margin > 40 ? 'medium' : 'low'}
{@const stockStatus =
item.stock <= item.reorderPoint * 0.5
? 'critical'
: item.stock <= item.reorderPoint
? 'low'
: 'ok'}
{@const inventoryValue = item.stock * item.cost}
{@const potentialRevenue = item.stock * item.price}
<tr class="inventory-row" data-status={stockStatus}>
<td class="sku">{item.sku}</td>
<td class="name">{item.name}</td>
<td class="price">${item.price.toFixed(2)}</td>
<td class="cost">${item.cost.toFixed(2)}</td>
<td class="margin {marginClass}">{margin.toFixed(1)}%</td>
<td class="stock">{item.stock} units</td>
<td class="status">
{#if stockStatus === 'critical'}
<span class="badge critical">⚠️ Critical</span>
{:else if stockStatus === 'low'}
<span class="badge warning">📦 Reorder</span>
{:else}
<span class="badge ok">✓ In Stock</span>
{/if}
</td>
<td class="value">
<div class="value-breakdown">
<span class="cost-value">${inventoryValue.toLocaleString()}</span>
<span class="potential">→ ${potentialRevenue.toLocaleString()}</span>
</div>
</td>
</tr>
{/each}
</tbody>
<tfoot>
<tr class="totals-row">
<td colspan="5">
<strong>Totals:</strong>
{sortedInventory.length} products, {totalItems.toLocaleString()} units
{#if lowStockCount > 0}
<span class="low-stock-warning">({lowStockCount} need reorder)</span>
{/if}
</td>
<td colspan="3">
<div class="total-values">
<span>Cost: ${totalCostValue.toLocaleString()}</span>
<span>Potential: ${totalPotentialRevenue.toLocaleString()}</span>
</div>
</td>
</tr>
</tfoot>
</table>
<style>
.inventory-table {
width: 100%;
border-collapse: collapse;
}
th {
cursor: pointer;
user-select: none;
padding: 0.75rem;
text-align: left;
background: #f8fafc;
}
th.active {
background: #e2e8f0;
}
td {
padding: 0.75rem;
border-bottom: 1px solid #e2e8f0;
}
.margin.high {
color: #16a34a;
font-weight: 600;
}
.margin.medium {
color: #ca8a04;
}
.margin.low {
color: #dc2626;
}
.inventory-row[data-status='critical'] {
background: #fef2f2;
}
.inventory-row[data-status='low'] {
background: #fffbeb;
}
.badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
}
.badge.critical {
background: #fee2e2;
color: #dc2626;
}
.badge.warning {
background: #fef3c7;
color: #b45309;
}
.badge.ok {
background: #dcfce7;
color: #16a34a;
}
.value-breakdown {
display: flex;
flex-direction: column;
font-size: 0.875rem;
}
.potential {
color: #6b7280;
}
.low-stock-warning {
color: #dc2626;
margin-left: 0.5rem;
}
</style> Common Pitfalls and How to Avoid Them
1: Using @const at the Top Level
<!-- CORRECT: Use script section for top-level constants -->
<script>
const greeting = 'Hello'
</script>
<!-- WRONG: @const cannot be used at top level -->
{@const greeting = 'Hello'}
<!-- Compiler error! -->
<h1>{greeting}</h1>
<h1>{greeting}</h1> 2: Attempting Reassignment
{#each items as item}
{@const count = item.quantity}
<!-- WRONG: Cannot reassign const -->
<button onclick={() => count++}>Increment</button>
<!-- Error! -->
<!-- CORRECT: Modify the source state instead -->
<button onclick={() => item.quantity++}>Increment</button>
{/each} 3: Expecting Reactivity from Constants
<script>
let multiplier = $state(1)
</script>
{#each items as item}
{@const computed = item.value * multiplier}
<!-- This works correctly - computed is recalculated when multiplier changes
because the entire each block re-renders -->
<span>{computed}</span>
{/each}
<!-- But be aware: @const doesn't create reactive bindings,
it creates values that exist for a single render cycle --> 4: Using @const Inside HTML Elements
<!-- WRONG: @const must be direct child of a block -->
<div>
{@const value = 42}
<!-- Compiler error! -->
{value}
</div>
<!-- CORRECT: Wrap in a block if needed -->
{#if true}
{@const value = 42}
<div>{value}</div>
{/if}
<!-- Or compute in the script section for simple cases --> 5: Overusing @const for Simple Expressions
<!-- Unnecessary: simple property access doesn't need @const -->
{#each users as user}
{@const name = user.name}
<!-- Overkill -->
<span>{name}</span>
{/each}
<!-- Better: use directly when expression is simple -->
{#each users as user}
<span>{user.name}</span>
{/each}
<!-- @const shines for computed values or repeated expressions -->
{#each users as user}
{@const displayName = `${user.firstName} ${user.lastName}`.trim() || 'Anonymous'}
{@const initials = displayName
.split(' ')
.map((n) => n[0])
.join('')}
<div class="user">
<span class="avatar">{initials}</span>
<span class="name">{displayName}</span>
</div>
{/each} Performance Considerations
The @const directive is computationally inexpensive—it’s essentially syntactic sugar that the compiler transforms into optimized JavaScript. However, keep these points in mind:
Calculation happens on every render: Unlike
$derived, which memoizes results,@constvalues are computed fresh each time the block renders. For expensive calculations, consider moving them to$derivedin the script section.No reactivity overhead: Since
@constdoesn’t create reactive bindings, there’s no subscription management overhead. This makes it efficient for values that don’t need to trigger updates themselves.Scope efficiency: Values declared with
@constare properly scoped and garbage collected when the block is destroyed, preventing memory leaks.
Best Practices Summary
Use
@constfor block-scoped computations that are only relevant within a specific iteration or conditional branch.Prefer
@constover inline repetition when the same calculation appears multiple times within a block.Chain constants when building up complex calculations step by step—each intermediate value becomes self-documenting.
Use destructuring to cleanly extract nested data at the point of use.
Keep component scripts focused on core state and cross-cutting concerns; let
@consthandle presentation logic.Don’t over-engineer: Simple property access doesn’t need
@const. Reserve it for actual computations or values used multiple times.Combine with
$derivedstrategically: use$derivedfor component-level transformations that affect what renders, and@constfor per-item presentation logic.
Conclusion
The @const directive embodies Svelte’s philosophy of putting the right tools in the right places. By enabling scoped constant declarations within template blocks, it helps you write cleaner, more readable, and more maintainable components—keeping computation close to consumption while maintaining clear separation of concerns.
This seemingly simple feature has profound implications for code organization: it lets you keep component scripts focused on state management while moving presentation-specific computations directly into the template where they’re used.
The strategic use of @const transforms verbose, repetitive templates into self-documenting, efficient code. Whether you’re formatting display values, computing intermediate results in #each iterations, or building up complex calculations step-by-step, @const provides the right level of abstraction without the overhead of reactive bindings.
By understanding when to use @const versus $derived, and how to leverage destructuring and chaining patterns, you can write Svelte components that are both performant and maintainable.
Key Takeaways
{@const}creates block-scoped constants within#if,#each,#await, and#snippetblocks, computed fresh on each render without reactive overhead- Syntax follows JavaScript const declarations with support for destructuring arrays and objects:
{@const { name, age } = user}or{@const [first, ...rest] = items} - Scope is limited to the declaring block and its children - constants are not accessible outside their defining block, preventing naming conflicts
- Calculations happen on every render unlike
$derivedwhich memoizes results, making@constbest for simple computations or values used multiple times within a block - No reactivity overhead means efficient rendering -
@constdoesn’t create subscriptions or trigger updates, just computes values when the block renders - Chainable for multi-step calculations where each
@constcan reference previous constants:{@const a = x + y} {@const b = a * 2} {@const c = b / z} - Common patterns include formatting (dates, numbers, currency), extracting nested data, computing totals in
#eachblocks, and simplifying complex conditionals - TypeScript integration provides full type inference with compile-time checking for undefined references and type errors in constant expressions
See Also
- Official Svelte 5 Documentation -
{@const} $derived- Reactive computed values at component level- JavaScript const - The JavaScript const keyword that
@constis based on - Destructuring Assignment - Pattern used extensively with
@const