Declarative Conditional Rendering in Svelte 5
Every user interface must make decisions. Show this button when the user is logged in. Display an error message when validation fails. Reveal advanced options when the user toggles a switch. These conditional rendering patterns are fundamental to building dynamic, responsive applications.
Svelte 5’s #if block provides an elegant, declarative approach to conditional rendering that integrates seamlessly with the framework’s reactivity system. Unlike imperative approaches where you manually manipulate the DOM or toggle CSS classes, Svelte’s template syntax lets you describe what should render under which conditions—the framework handles the how.
This tutorial explores the #if block comprehensively: from basic conditionals to complex multi-branch logic, from reactive state integration to transition effects, from common pitfalls to advanced patterns. Whether you’re building simple toggle interfaces or complex permission-based UIs, you’ll master the techniques needed to render content conditionally with confidence.
The Problem
Why Declarative Conditionals Matter
Before diving into syntax, let’s understand why declarative conditional rendering matters. Consider a traditional JavaScript approach:
// Imperative approach - managing DOM directly
function updateUI(isLoggedIn) {
const loginButton = document.getElementById('login-btn')
const userPanel = document.getElementById('user-panel')
if (isLoggedIn) {
loginButton.style.display = 'none'
userPanel.style.display = 'block'
} else {
loginButton.style.display = 'block'
userPanel.style.display = 'none'
}
} This approach suffers from several issues:
- Scattered logic: UI state management is spread across JavaScript and HTML
- Manual synchronization: You must remember to call
updateUI()whenever state changes - Error-prone: Easy to forget updating one element when conditions change
- Poor readability: The relationship between data and UI isn’t immediately clear
Svelte’s declarative approach eliminates these problems:
<script>
let isLoggedIn = $state(false)
</script>
{#if isLoggedIn}
<div id="user-panel">Welcome back!</div>
{:else}
<button id="login-btn" onclick={() => (isLoggedIn = true)}> Login </button>
{/if} The UI automatically reflects the state. Change isLoggedIn, and Svelte updates the DOM. The relationship between data and rendering is explicit in the template.
Basic Syntax
Simple If Block
The most basic form conditionally renders content when an expression is truthy:
{#if expression}
<!-- Content rendered when expression is truthy -->
{/if} <script>
let showMessage = $state(true)
</script>
<button onclick={() => (showMessage = !showMessage)}> Toggle Message </button>
{#if showMessage}
<p>Hello, this message is visible!</p>
{/if} The content inside the block—including all elements, text, and even other components—is only rendered when showMessage is true. When it becomes false, Svelte removes the content from the DOM entirely.
If-Else Block
For binary states where you need to show one thing or another:
{#if expression}
<!-- Rendered when truthy -->
{:else}
<!-- Rendered when falsy -->
{/if} <script>
let isOnline = $state(false)
</script>
<div class="status-indicator">
{#if isOnline}
<span class="online">Online</span>
{:else}
<span class="offline">Offline</span>
{/if}
</div>
<button onclick={() => (isOnline = !isOnline)}> Toggle Status </button> Multi-Branch Conditions
For multiple exclusive conditions, chain with {:else if}:
{#if expression1}
<!-- First condition -->
{:else if expression2}
<!-- Second condition -->
{:else if expression3}
<!-- Third condition -->
{:else}
<!-- Default fallback -->
{/if} <script>
let temperature = $state(72)
</script>
<input type="range" bind:value={temperature} min="0" max="120" />
<p>Temperature: {temperature}°F</p>
{#if temperature > 100}
<p class="danger">🔥 Dangerously hot!</p>
{:else if temperature > 80}
<p class="warm">☀️ It's warm today</p>
{:else if temperature > 60}
<p class="comfortable">😊 Perfect weather</p>
{:else if temperature > 40}
<p class="cool">🍂 Getting chilly</p>
{:else}
<p class="cold">❄️ Bundle up!</p>
{/if} Conditions are evaluated in order—the first truthy expression wins, and subsequent conditions are not checked. This is important for performance and logic correctness.
Truthy and Falsy Values
Svelte’s #if block uses JavaScript’s truthiness evaluation. Understanding what’s truthy and falsy helps avoid bugs:
Falsy Values
These values evaluate to false:
false0(zero)-0(negative zero)0n(BigInt zero)""(empty string)nullundefinedNaN
Truthy Values
Everything else is truthy, including:
true- Any non-zero number (including negative numbers)
- Any non-empty string (including
"false"and"0") - Objects and arrays (even empty ones:
{}and[]) - Functions
<script>
let count = $state(0)
let items = $state([])
let user = $state(null)
let message = $state('')
</script>
<!-- Common pitfalls with truthiness -->
{#if count}
<p>Count is {count}</p>
{:else}
<p>Count is zero or falsy</p>
{/if}
<!-- 0 is falsy! This shows "Count is zero or falsy" -->
{#if items}
<p>Items array exists (length: {items.length})</p>
{/if}
<!-- Empty arrays are truthy! This always shows -->
{#if items.length}
<p>There are {items.length} items</p>
{:else}
<p>No items yet</p>
{/if}
<!-- Check .length for "has items" logic -->
{#if user}
<p>Welcome, {user.name}!</p>
{:else}
<p>Please log in</p>
{/if}
<!-- null is falsy, this works as expected -->
{#if message}
<p>{message}</p>
{/if}
<!-- Empty string is falsy, nothing renders --> Explicit Boolean Conversion
When truthiness semantics might be confusing, be explicit:
<script>
let score = $state(0)
</script>
<!-- Problematic: 0 is a valid score but falsy -->
{#if score}
<p>Your score: {score}</p>
{/if}
<!-- Better: explicit check for what you mean -->
{#if score !== null && score !== undefined}
<p>Your score: {score}</p>
{/if}
<!-- Or use typeof for number check -->
{#if typeof score === 'number'}
<p>Your score: {score}</p>
{/if} Reactivity Integration with Runes
The #if block automatically responds to reactive state changes. With Svelte 5’s runes, this integration is seamless and intuitive.
Basic Reactive Conditionals
<script>
let count = $state(0)
let threshold = $state(5)
</script>
<div class="controls">
<button onclick={() => count--}>-</button>
<span>{count}</span>
<button onclick={() => count++}>+</button>
</div>
<input type="number" bind:value={threshold} />
{#if count > threshold}
<p class="success">Count ({count}) exceeds threshold ({threshold})!</p>
{:else if count === threshold}
<p class="warning">Count equals threshold exactly</p>
{:else}
<p class="info">Count is below threshold (need {threshold - count} more)</p>
{/if} When either count or threshold changes, Svelte re-evaluates the conditions and updates the DOM accordingly.
Derived State in Conditions
Use $derived for computed conditions:
<script>
let password = $state('')
let confirmPassword = $state('')
let passwordStrength = $derived.by(() => {
if (password.length === 0) return 'empty'
if (password.length < 6) return 'weak'
if (password.length < 10) return 'medium'
if (/[A-Z]/.test(password) && /[0-9]/.test(password)) return 'strong'
return 'medium'
})
let passwordsMatch = $derived(password === confirmPassword && password.length > 0)
let canSubmit = $derived(passwordStrength !== 'weak' && passwordsMatch)
</script>
<input type="password" bind:value={password} placeholder="Password" />
<input type="password" bind:value={confirmPassword} placeholder="Confirm" />
{#if passwordStrength === 'empty'}
<p class="hint">Enter a password</p>
{:else if passwordStrength === 'weak'}
<p class="error">Password too weak (min 6 characters)</p>
{:else if passwordStrength === 'medium'}
<p class="warning">Password could be stronger</p>
{:else}
<p class="success">Strong password!</p>
{/if}
{#if password.length > 0}
{#if passwordsMatch}
<p class="success">Passwords match</p>
{:else if confirmPassword.length > 0}
<p class="error">Passwords don't match</p>
{/if}
{/if}
<button disabled={!canSubmit}>
{#if canSubmit}
Create Account
{:else}
Please complete form
{/if}
</button> Complex Object State
Conditionals work seamlessly with object and array state:
<script>
let user = $state(null)
async function login() {
// Simulate API call
await new Promise((r) => setTimeout(r, 1000))
user = {
id: 1,
name: 'Alice',
role: 'admin',
permissions: ['read', 'write', 'delete']
}
}
function logout() {
user = null
}
</script>
{#if user === null}
<div class="login-panel">
<h2>Welcome, Guest</h2>
<button onclick={login}>Login</button>
</div>
{:else}
<div class="user-panel">
<h2>Welcome, {user.name}!</h2>
<p>Role: {user.role}</p>
{#if user.role === 'admin'}
<div class="admin-section">
<h3>Admin Controls</h3>
{#if user.permissions.includes('delete')}
<button class="danger">Delete All Data</button>
{/if}
</div>
{/if}
<button onclick={logout}>Logout</button>
</div>
{/if} Nesting #if Blocks
the #if blocks can be nested to any depth, enabling complex conditional logic:
<script>
let user = $state({ loggedIn: true, isPremium: true, hasTrial: false })
let feature = $state({ enabled: true, requiresPremium: true })
</script>
{#if user.loggedIn}
<div class="dashboard">
<h1>Dashboard</h1>
{#if feature.enabled}
{#if feature.requiresPremium}
{#if user.isPremium}
<div class="premium-feature">
<h2>Premium Feature</h2>
<p>Enjoy your exclusive content!</p>
</div>
{:else if user.hasTrial}
<div class="trial-feature">
<h2>Trial Access</h2>
<p>You're using a free trial of this feature.</p>
<button>Upgrade to Premium</button>
</div>
{:else}
<div class="upgrade-prompt">
<h2>Premium Required</h2>
<p>This feature requires a premium subscription.</p>
<button>Start Free Trial</button>
<button>Upgrade Now</button>
</div>
{/if}
{:else}
<div class="standard-feature">
<h2>Standard Feature</h2>
<p>Available to all users!</p>
</div>
{/if}
{:else}
<div class="feature-disabled">
<p>This feature is currently unavailable.</p>
</div>
{/if}
</div>
{:else}
<div class="login-prompt">
<h1>Please Log In</h1>
<p>You need to be logged in to access the dashboard.</p>
<button>Login</button>
</div>
{/if} While nesting is powerful, deeply nested conditionals can become hard to read. Consider extracting complex logic into derived state or separate components.
Conditional Rendering with Transitions
Svelte’s transition directives work beautifully with #if blocks, animating content as it enters and exits:
<script>
import { fade, fly, slide, scale } from 'svelte/transition'
let showNotification = $state(false)
let showSidebar = $state(false)
let showModal = $state(false)
</script>
<!-- Fade transition -->
<button onclick={() => (showNotification = !showNotification)}> Toggle Notification </button>
{#if showNotification}
<div class="notification" transition:fade={{ duration: 300 }}>
<p>This notification fades in and out!</p>
</div>
{/if}
<!-- Fly transition -->
<button onclick={() => (showSidebar = !showSidebar)}> Toggle Sidebar </button>
{#if showSidebar}
<aside transition:fly={{ x: -300, duration: 400 }}>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</aside>
{/if}
<!-- Scale transition for modals -->
<button onclick={() => (showModal = !showModal)}> Open Modal </button>
{#if showModal}
<div class="modal-backdrop" transition:fade={{ duration: 200 }}>
<div class="modal" transition:scale={{ start: 0.8, duration: 300 }}>
<h2>Modal Title</h2>
<p>Modal content goes here.</p>
<button onclick={() => (showModal = false)}>Close</button>
</div>
</div>
{/if} Separate in: and out: Transitions
Use in: and out: for different enter/exit animations:
<script>
import { fly, fade } from 'svelte/transition'
let items = $state([
{ id: 1, text: 'First item' },
{ id: 2, text: 'Second item' },
{ id: 3, text: 'Third item' }
])
let showList = $state(true)
</script>
<button onclick={() => (showList = !showList)}>
{showList ? 'Hide' : 'Show'} List
</button>
{#if showList}
<ul>
{#each items as item (item.id)}
<li in:fly={{ y: 20, duration: 300 }} out:fade={{ duration: 200 }}>
{item.text}
</li>
{/each}
</ul>
{/if} Conditional class vs #if Blocks
Sometimes you want to keep an element in the DOM but toggle its visibility. Compare these approaches:
<script>
let isVisible = $state(true)
</script>
<!-- Approach 1: {#if} block - removes from DOM -->
{#if isVisible}
<div class="panel">I'm removed when hidden</div>
{/if}
<!-- Approach 2: CSS class toggle - stays in DOM -->
<div class="panel" class:hidden={!isVisible}>I'm always in DOM, just hidden</div>
<!-- Approach 3: Inline style -->
<div class="panel" style:display={isVisible ? 'block' : 'none'}>
I'm always in DOM, display toggled
</div>
<style>
.hidden {
display: none;
}
</style> When to use each:
#ifblock: When the hidden content is expensive to keep around (complex components, many DOM nodes) or when you need enter/exit transitions- CSS class/style: When you need instant show/hide, when preserving component state is important, or when the content is simple
Inline Text and Elements
If blocks don’t require wrapping elements—they can conditionally render inline content:
<script>
let user = $state({ name: 'Alice', isPremium: true })
let unreadCount = $state(5)
</script>
<p>
Welcome, {user.name}{#if user.isPremium}
⭐{/if}!
</p>
<button>
Messages
{#if unreadCount > 0}
<span class="badge">{unreadCount}</span>
{/if}
</button>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
{#if user.isPremium}
<a href="/premium">Premium Content</a>
{/if}
<a href="/contact">Contact</a>
</nav> Common Patterns and Use Cases
1. Loading States
Problem: Users need feedback while data is loading, and clear error or empty states if loading fails or returns nothing. Solution: Use #if/{:else if}/{:else} to show skeletons, errors, loaded data, or prompts as appropriate.
<script>
let isLoading = $state(true)
let data = $state(null)
let error = $state(null)
async function fetchData() {
isLoading = true
error = null
try {
const response = await fetch('/api/data')
if (!response.ok) throw new Error('Failed to fetch')
data = await response.json()
} catch (e) {
error = e.message
} finally {
isLoading = false
}
}
</script>
<button onclick={fetchData}>Load Data</button>
{#if isLoading}
<div class="skeleton-loader">
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
<div class="skeleton-line"></div>
</div>
{:else if error}
<div class="error-message">
<p>Error: {error}</p>
<button onclick={fetchData}>Retry</button>
</div>
{:else if data}
<div class="data-display">
<h2>{data.title}</h2>
<p>{data.content}</p>
</div>
{:else}
<p>Click the button to load data.</p>
{/if} 2. Form Validation Feedback
Problem: Users need immediate, contextual feedback when entering form data, especially for validation errors or success. Solution: Use derived state and #if/{:else if} to show hints, errors, or success messages as the user interacts with the form.
<script>
let email = $state('')
let touched = $state(false)
let isValid = $derived(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
let showError = $derived(touched && !isValid && email.length > 0)
let showSuccess = $derived(touched && isValid)
</script>
<div class="form-field">
<label for="email">Email Address</label>
<input
id="email"
type="email"
bind:value={email}
onblur={() => (touched = true)}
class={{ error: showError, success: showSuccess }}
/>
{#if showError}
<p class="error-text">Please enter a valid email address</p>
{:else if showSuccess}
<p class="success-text">✓ Valid email format</p>
{:else if !touched}
<p class="hint-text">We'll never share your email</p>
{/if}
</div>
<style>
.error {
border-color: #dc2626;
}
.success {
border-color: #16a34a;
}
.error-text {
color: #dc2626;
}
.success-text {
color: #16a34a;
}
.hint-text {
color: #6b7280;
}
</style> 3. Permission-Based UI
Problem: Not all users have the same permissions—UI should adapt to show/hide controls based on user roles and permissions. Solution: Use #if blocks to conditionally render navigation links and controls based on permission checks.
<script>
let user = $state({
role: 'editor',
permissions: ['read', 'write', 'publish']
})
function hasPermission(permission) {
return user.permissions.includes(permission)
}
</script>
<nav class="admin-nav">
<a href="/dashboard">Dashboard</a>
{#if hasPermission('read')}
<a href="/content">View Content</a>
{/if}
{#if hasPermission('write')}
<a href="/content/new">Create Content</a>
{/if}
{#if hasPermission('publish')}
<a href="/publish">Publish Queue</a>
{/if}
{#if user.role === 'admin'}
<a href="/admin" class="admin-link">Admin Panel</a>
{/if}
</nav>
<main>
<h1>Content Editor</h1>
<article>
<h2>Article Title</h2>
<p>Article content...</p>
{#if hasPermission('write')}
<div class="edit-controls">
<button>Edit</button>
{#if hasPermission('delete')}
<button class="danger">Delete</button>
{/if}
</div>
{/if}
{#if hasPermission('publish')}
<div class="publish-controls">
<button class="primary">Publish</button>
<button>Schedule</button>
</div>
{/if}
</article>
</main> 4. Responsive/Feature Detection
Problem: UI should adapt to device capabilities (touch vs. mouse) and screen size (mobile/tablet/desktop). Solution: Use #if blocks to render different UI elements or hints based on environment or feature detection.
<script>
import { browser } from '$app/environment'
let isTouchDevice = $state(false)
let screenSize = $state('desktop')
if (browser) {
isTouchDevice = 'ontouchstart' in window
function updateScreenSize() {
if (window.innerWidth < 640) screenSize = 'mobile'
else if (window.innerWidth < 1024) screenSize = 'tablet'
else screenSize = 'desktop'
}
updateScreenSize()
window.addEventListener('resize', updateScreenSize)
}
</script>
<nav>
{#if screenSize === 'mobile'}
<button class="hamburger-menu">☰</button>
{:else}
<div class="nav-links">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</div>
{/if}
</nav>
<div class="interaction-hint">
{#if isTouchDevice}
<p>👆 Tap items to interact</p>
{:else}
<p>🖱️ Click items to interact</p>
{/if}
</div> 5. Empty States
Problem: When a list or search returns no results, users need clear feedback and guidance on what to do next. Solution: Use #if/{:else if}/{:else} to show initial, empty, or results states for better UX.
<script>
let searchQuery = $state('')
let searchResults = $state([])
let hasSearched = $state(false)
</script>
<input type="search" bind:value={searchQuery} placeholder="Search..." />
<button
onclick={() => {
/* perform search */ hasSearched = true
}}
>
Search
</button>
{#if !hasSearched}
<div class="initial-state">
<img src="/search-illustration.svg" alt="" />
<p>Enter a search term to find results</p>
</div>
{:else if searchResults.length === 0}
<div class="empty-state">
<img src="/no-results.svg" alt="" />
<h3>No results found</h3>
<p>Try adjusting your search terms</p>
</div>
{:else}
<ul class="results">
{#each searchResults as result}
<li>{result.title}</li>
{/each}
</ul>
{/if} 6. Feature Flags
Problem: You may want to enable or disable features for certain users or environments without changing code structure. Solution: Use #if blocks to conditionally render components or UI based on feature flag values.
<script>
// Feature flags could come from environment, API, or config
const features = {
newDashboard: true,
darkMode: true,
betaExport: false,
experimentalAI: false
}
</script>
{#if features.newDashboard}
<DashboardV2 />
{:else}
<DashboardV1 />
{/if}
<div class="settings">
{#if features.darkMode}
<label>
<input type="checkbox" /> Dark Mode
</label>
{/if}
{#if features.betaExport}
<button>Export (Beta)</button>
{/if}
{#if features.experimentalAI}
<div class="ai-assistant">
<button>Ask AI ✨</button>
</div>
{/if}
</div> Using @const Inside If Blocks
The @const directive allows you to declare block-scoped constants, which is particularly useful inside #if blocks:
<script>
let order = $state({
items: [
{ name: 'Widget', price: 29.99, quantity: 2 },
{ name: 'Gadget', price: 49.99, quantity: 1 }
],
coupon: { code: 'SAVE10', discount: 0.1 }
})
</script>
{#if order.items.length > 0}
{@const subtotal = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0)}
{@const discount = order.coupon ? subtotal * order.coupon.discount : 0}
{@const total = subtotal - discount}
<div class="order-summary">
<h2>Order Summary</h2>
{#each order.items as item}
<div class="line-item">
<span>{item.name} × {item.quantity}</span>
<span>${(item.price * item.quantity).toFixed(2)}</span>
</div>
{/each}
<hr />
<div class="subtotal">
<span>Subtotal</span>
<span>${subtotal.toFixed(2)}</span>
</div>
{#if order.coupon}
{@const savings = discount.toFixed(2)}
<div class="discount">
<span>Discount ({order.coupon.code})</span>
<span class="savings">-${savings}</span>
</div>
{/if}
<div class="total">
<span>Total</span>
<span>${total.toFixed(2)}</span>
</div>
</div>
{:else}
<div class="empty-cart">
<p>Your cart is empty</p>
<a href="/shop">Continue Shopping</a>
</div>
{/if} Common Struggles and Solutions
1: Checking for Array Length
Problem: Empty arrays are truthy in JavaScript, so {#if items} is always true—even when the array is empty. Solution: Always check .length to determine if an array has items.
<script>
let items = $state([])
</script>
<!-- AVOID: Empty arrays are truthy! -->
{#if items}
<p>You have items!</p>
{/if}
<!-- This always shows, even when items is empty -->
<!-- PREFERRED: Check the length -->
{#if items.length > 0}
<p>You have {items.length} items!</p>
{:else}
<p>No items yet.</p>
{/if}
<!-- PREFERRED: Length is falsy when 0 -->
{#if items.length}
<p>You have {items.length} items!</p>
{/if} 2: Checking for Object Properties
Problem: Empty strings are falsy, so {#if user.email} fails when the property exists but is an empty string. Solution: Be explicit—check for undefined/null or use 'property' in object to test for property existence.
<script>
let user = $state({ name: 'Alice', email: '' })
</script>
<!-- AVOID: Empty string is falsy -->
{#if user.email}
<p>Email: {user.email}</p>
{:else}
<p>No email provided</p>
{/if}
<!-- Shows "No email provided" when email is "" -->
<!-- PREFERRED: Be explicit about what you're checking -->
{#if user.email !== undefined && user.email !== null}
<p>Email: {user.email || '(empty)'}</p>
{:else}
<p>Email not set</p>
{/if}
<!-- PREFERRED: Or check for the property existing -->
{#if 'email' in user}
<p>Email: {user.email || '(not provided)'}</p>
{/if} 3: Optional Chaining in Conditions
Problem: Accessing nested properties in conditions can throw errors if any parent is null or undefined. Solution: Use optional chaining (?.) or chain checks to safely access deeply nested values.
<script>
let data = $state(null)
// Later: data = { user: { profile: { avatar: 'url' } } }
</script>
<!-- AVOID: Will throw if data or user is null -->
{#if data.user.profile.avatar}
<img src={data.user.profile.avatar} alt="Avatar" />
{/if}
<!-- PREFERRED: Use optional chaining -->
{#if data?.user?.profile?.avatar}
<img src={data.user.profile.avatar} alt="Avatar" />
{/if}
<!-- PREFERRED: Chain the checks -->
{#if data && data.user && data.user.profile && data.user.profile.avatar}
<img src={data.user.profile.avatar} alt="Avatar" />
{/if} 4: Comparing Objects
Problem: Comparing objects with === checks reference, not value—so two objects with the same data are not equal unless they are the same reference. Solution: Compare by unique identifier (like id), or ensure you use the same reference.
<script>
let selectedItem = $state({ id: 1, name: 'Widget' });
let items = $state([
{ id: 1, name: 'Widget' },
{ id: 2, name: 'Gadget' }
]);
</script>
<!-- AVOID: Object comparison checks reference, not value -->
{#each items as item}
{#if item === selectedItem}
<p>Selected!</p>
{/if}
{/each}
<!-- Never matches because they're different object references -->
<!-- PREFERRED: Compare by identifier -->
{#each items as item}
{#if item.id === selectedItem.id}
<p>Selected!</p>
{/if}
{/each}
<!-- PREFERRED: Use the same reference -->
<script>
let items = $state([
{ id: 1, name: 'Widget' },
{ id: 2, name: 'Gadget' }
]);
let selectedItem = $state(items[0]); // Same reference
</script> 5: Async Data in Conditions
Problem: When loading async data, you can’t distinguish between “loading”, “no data”, and “data loaded” with a single variable. Solution: Use an explicit loading state variable to differentiate between loading, loaded, and empty states.
<script>
let user = $state(undefined); // Loading state
async function loadUser() {
const response = await fetch('/api/user');
user = await response.json();
}
loadUser();
</script>
<!-- AVOID: Can't distinguish loading from no user -->
{#if user}
<p>Welcome, {user.name}!</p>
{:else}
<p>Please log in</p>
{/if}
<!-- PREFERRED: Use explicit loading state -->
<script>
let user = $state(null);
let isLoading = $state(true);
async function loadUser() {
isLoading = true;
try {
const response = await fetch('/api/user');
user = await response.json();
} finally {
isLoading = false;
}
}
loadUser();
</script>
{#if isLoading}
<p>Loading...</p>
{:else if user}
<p>Welcome, {user.name}!</p>
{:else}
<p>Please log in</p>
{/if} 6: Multiple Conditions That Could All Be True
Problem: Using {:else if} for non-exclusive conditions means only the first true condition is shown, even if others are also true. Solution: Use separate #if blocks for each condition when multiple can be true at once.
<script>
let notifications = $state({
hasError: true,
hasWarning: true,
hasInfo: true
})
</script>
<!-- AVOID: Only shows first match -->
{#if notifications.hasError}
<p class="error">Error occurred!</p>
{:else if notifications.hasWarning}
<p class="warning">Warning!</p>
{:else if notifications.hasInfo}
<p class="info">FYI...</p>
{/if}
<!-- Only shows error, even though all are true -->
<!-- PREFERRED: Use separate if blocks for non-exclusive conditions -->
{#if notifications.hasError}
<p class="error">Error occurred!</p>
{/if}
{#if notifications.hasWarning}
<p class="warning">Warning!</p>
{/if}
{#if notifications.hasInfo}
<p class="info">FYI...</p>
{/if} Performance Considerations
DOM Creation and Destruction
Each time an #if condition changes, Svelte creates or destroys the DOM nodes inside. For simple content, this is fast. For complex components with many children, consider:
<script>
let showPanel = $state(false)
</script>
<!-- This destroys and recreates the component each toggle -->
{#if showPanel}
<ExpensiveComponent />
{/if}
<!-- Alternative: Keep component alive, just hide it -->
<div style:display={showPanel ? 'block' : 'none'}>
<ExpensiveComponent />
</div>
<!-- Or use CSS -->
<div class:hidden={!showPanel}>
<ExpensiveComponent />
</div>
<style>
.hidden {
display: none;
}
</style> Avoiding Unnecessary Recalculations
When conditions depend on complex expressions, extract them to $derived:
<script>
let items = $state([
/* many items */
])
let filter = $state('')
// AVOID: Recalculated on every render
// {#if items.filter(i => i.name.includes(filter)).length > 0}
// PREFERRED: Only recalculated when items or filter change
let filteredItems = $derived(items.filter((i) => i.name.includes(filter)))
let hasResults = $derived(filteredItems.length > 0)
</script>
{#if hasResults}
<ul>
{#each filteredItems as item}
<li>{item.name}</li>
{/each}
</ul>
{:else}
<p>No results</p>
{/if} Best Practices Summary
Be explicit about what you’re checking: Avoid relying on implicit truthiness for values like
0, empty strings, or empty arrays when those are valid values.Use optional chaining: When accessing nested properties that might not exist, use
?.to prevent runtime errors.Extract complex conditions to
$derived: This improves readability and ensures efficient updates.Consider the DOM lifecycle: Understand that
#ifcreates/destroys nodes. Use CSS visibility for frequently toggled, expensive content.Don’t over-nest: If you have many levels of nesting, consider extracting logic to derived state or separate components.
Use
{:else if}for mutually exclusive states: This is more efficient than multiple separate#ifblocks.Use separate
#ifblocks for non-exclusive conditions: When multiple conditions can be true simultaneously.Provide meaningful empty/loading states: Users appreciate knowing what’s happening rather than seeing nothing.
Combine with transitions for polish: Animate conditional content for a smoother user experience.
Document complex conditions: If the logic isn’t immediately obvious, add comments explaining the business logic.
Quick Reference
<!-- Basic if -->
{#if condition}
<p>Rendered when true</p>
{/if}
<!-- If-else -->
{#if condition}
<p>True branch</p>
{:else}
<p>False branch</p>
{/if}
<!-- Multiple branches -->
{#if condition1}
<p>First match</p>
{:else if condition2}
<p>Second match</p>
{:else if condition3}
<p>Third match</p>
{:else}
<p>Default</p>
{/if}
<!-- Inline conditional -->
<p>
Status: {#if active}Active{:else}Inactive{/if}
</p>
<!-- With transitions -->
{#if visible}
<div transition:fade>Animated content</div>
{/if}
<!-- With @const -->
{#if items.length > 0}
{@const total = items.reduce((a, b) => a + b, 0)}
<p>Total: {total}</p>
{/if}
<!-- Nested -->
{#if user}
{#if user.isAdmin}
<AdminPanel />
{:else}
<UserDashboard />
{/if}
{:else}
<LoginForm />
{/if} Conclusion
The #if block is deceptively simple in syntax but fundamental to building dynamic UIs. By understanding truthiness, embracing Svelte’s reactivity, and following best practices, you can create interfaces that respond elegantly to any condition your application encounters. Its declarative nature transforms what would be imperative show/hide logic in vanilla JavaScript into clean, readable template syntax that automatically responds to state changes.
The power of #if blocks lies not just in their simplicity, but in how they compose with Svelte’s reactive system. Combined with $derived for complex conditions, @const for block-scoped calculations, and transitions for smooth animations, conditional rendering becomes a first-class architectural pattern rather than an afterthought. By mastering the nuances of truthiness, avoiding common pitfalls like excessive nesting, and understanding when to use {:else if} versus separate blocks, you can build interfaces that are both performant and maintainable.
Key Takeaways
#ifblocks conditionally render content based on JavaScript truthiness, with automatic reactivity when conditions change - content is added/removed from the DOM, not just hidden- Three-way branching uses
{:else if}and{:else}with unlimited chained conditions:{#if}{:else if}{:else if}{:else}{/if} - Truthiness follows JavaScript rules -
0,"",null,undefined,NaN, andfalseare falsy; everything else (including[],{},"0") is truthy - Combine with transitions for smooth UI changes -
transition:fade,in:fly, orout:slidedirectives animate content as it enters/exits the DOM - Reactive conditions using
$derivedenable complex computed predicates that automatically update:let showAdvanced = $derived(user?.role === 'admin' && settings.expertMode) - Block-scoped
@constenables inline calculations within conditions:{#if items.length > 0}{@const total = sum(items)}<p>Total: {total}</p>{/if} - Performance considerations: avoid expensive condition checks in templates - move complex calculations to
$derivedin the script section to prevent re-computation on every render - Nested conditions can hurt readability - consider component extraction,
$derivedpredicates, or guard clauses when nesting exceeds 2-3 levels
See Also
- Official Svelte 5 Documentation -
{#if} - JavaScript Truthy/Falsy - Understanding conditional evaluation
$derived- Computed reactive values for complex conditions- Svelte Transitions - Animating conditional content