Master Svelte from the ground up. Learn why Svelte was created, understand core concepts like reactive declarations, two-way binding, stores, components, and lifecycle hooks. Build a complete production-ready weather dashboard app covering all Svelte fundamentals with best practices.

Svelte represents a paradigm shift in frontend development. Unlike traditional frameworks that run in the browser, Svelte shifts work to compile time, resulting in smaller bundles and better performance. But why does Svelte exist, and what makes it fundamentally different?
In this article, we'll explore Svelte's philosophy, understand why it was created, dive deep into core concepts, and build a complete production-ready weather dashboard application that demonstrates all fundamental Svelte patterns.
Before Svelte, frontend development faced significant challenges:
Rich Harris created Svelte in 2016 with a revolutionary approach:
Reactive declarations automatically update when dependencies change. This is Svelte's most powerful feature.
<script>
let count = 0;
let doubled = 0;
// ✅ Reactive declaration - updates when count changes
$: doubled = count * 2;
// ✅ Reactive block - runs when dependencies change
$: {
console.log(`count is now ${count}`);
}
// ✅ Reactive if statement
$: if (count > 10) {
console.log('count is greater than 10');
}
</script>
<button on:click={() => count++}>
Count: {count}, Doubled: {doubled}
</button>Two-way binding synchronizes component state with form inputs automatically.
<script>
let name = '';
let email = '';
let agreed = false;
// ✅ Two-way binding with bind:value
// Changes to input automatically update the variable
// Changes to the variable automatically update the input
</script>
<input bind:value={name} placeholder="Enter name" />
<input bind:value={email} type="email" placeholder="Enter email" />
<input bind:checked={agreed} type="checkbox" />
<p>Name: {name}</p>
<p>Email: {email}</p>
<p>Agreed: {agreed}</p>Lifecycle hooks let you run code at specific times in a component's life.
<script>
import { onMount, onDestroy, beforeUpdate, afterUpdate } from 'svelte';
let data = null;
// ✅ Runs after component is mounted to the DOM
onMount(async () => {
const response = await fetch('/api/data');
data = await response.json();
// Return cleanup function
return () => {
console.log('Component unmounted');
};
});
// ✅ Runs before component updates
beforeUpdate(() => {
console.log('Component about to update');
});
// ✅ Runs after component updates
afterUpdate(() => {
console.log('Component updated');
});
// ✅ Runs when component is destroyed
onDestroy(() => {
console.log('Component destroyed');
});
</script>
{#if data}
<p>{data.message}</p>
{/if}Stores provide reactive state management across components.
// stores.ts
import { writable, readable, derived } from 'svelte/store';
// ✅ Writable store - can be updated from anywhere
export const count = writable(0);
// ✅ Readable store - can only be updated internally
export const time = readable(new Date(), (set) => {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return () => clearInterval(interval);
});
// ✅ Derived store - computed from other stores
export const doubled = derived(count, ($count) => $count * 2);
// Usage in component
<script>
import { count, doubled } from './stores';
</script>
<p>Count: {$count}</p>
<p>Doubled: {$doubled}</p>
<button on:click={() => count.update(n => n + 1)}>
Increment
</button>Components are reusable pieces of UI that accept props.
// Button.svelte
<script>
export let label = 'Click me';
export let disabled = false;
export let variant = 'primary';
</script>
<button class={variant} {disabled}>
{label}
</button>
<style>
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.primary {
background: #667eea;
color: white;
}
.primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
// App.svelte
<script>
import Button from './Button.svelte';
</script>
<Button label="Submit" variant="primary" />
<Button label="Cancel" variant="secondary" disabled />Handle user interactions with event directives.
<script>
let count = 0;
function handleClick() {
count++;
}
function handleSubmit(event) {
event.preventDefault();
console.log('Form submitted');
}
function handleKeydown(event) {
if (event.key === 'Enter') {
console.log('Enter pressed');
}
}
</script>
<button on:click={handleClick}>
Clicked {count} times
</button>
<form on:submit={handleSubmit}>
<input on:keydown={handleKeydown} />
<button type="submit">Submit</button>
</form>
<!-- Event modifiers -->
<button on:click|once={handleClick}>
Click once
</button>
<button on:click|preventDefault={() => console.log('clicked')}>
Prevent default
</button>Render different content based on conditions.
<script>
let isLoggedIn = false;
let count = 0;
</script>
<!-- if/else block -->
{#if isLoggedIn}
<p>Welcome back!</p>
{:else}
<p>Please log in</p>
{/if}
<!-- if/else if/else -->
{#if count === 0}
<p>No items</p>
{:else if count === 1}
<p>One item</p>
{:else}
<p>{count} items</p>
{/if}
<!-- Ternary-like with each -->
{#if items.length > 0}
<ul>
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
</ul>
{:else}
<p>No items found</p>
{/if}Render lists efficiently with the each block.
<script>
let todos = [
{ id: 1, title: 'Learn Svelte', completed: false },
{ id: 2, title: 'Build an app', completed: false },
];
function addTodo() {
todos = [...todos, {
id: todos.length + 1,
title: 'New todo',
completed: false
}];
}
</script>
<!-- Basic each -->
{#each todos as todo (todo.id)}
<div>
<input type="checkbox" bind:checked={todo.completed} />
<span class:completed={todo.completed}>
{todo.title}
</span>
</div>
{/each}
<!-- With index -->
{#each todos as todo, index (todo.id)}
<p>{index + 1}. {todo.title}</p>
{/each}
<!-- With else -->
{#each todos as todo (todo.id)}
<p>{todo.title}</p>
{:else}
<p>No todos</p>
{/each}
<button on:click={addTodo}>Add Todo</button>
<style>
.completed {
text-decoration: line-through;
opacity: 0.5;
}
</style>Add smooth animations and transitions to elements.
<script>
import { fade, slide, scale } from 'svelte/transition';
import { flip } from 'svelte/animate';
let visible = true;
let items = [1, 2, 3];
function removeItem(index) {
items = items.filter((_, i) => i !== index);
}
</script>
<!-- Fade transition -->
{#if visible}
<div transition:fade>
This fades in and out
</div>
{/if}
<!-- Slide transition -->
{#if visible}
<div transition:slide>
This slides in and out
</div>
{/if}
<!-- Scale transition with options -->
{#if visible}
<div transition:scale={{ duration: 300 }}>
This scales in and out
</div>
{/if}
<!-- Animate directive for list reordering -->
{#each items as item (item)}
<div animate:flip={{ duration: 200 }}>
{item}
<button on:click={() => removeItem(items.indexOf(item))}>
Remove
</button>
</div>
{/each}
<button on:click={() => visible = !visible}>
Toggle
</button>Styles are scoped to components by default.
<script>
let isActive = false;
</script>
<div class="container">
<h1>Scoped Styles</h1>
<button class:active={isActive} on:click={() => isActive = !isActive}>
Toggle
</button>
</div>
<style>
/* ✅ Scoped to this component only */
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
border-radius: 8px;
}
h1 {
color: #333;
font-size: 2rem;
}
button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: #5568d3;
transform: translateY(-2px);
}
/* ✅ Class binding for dynamic styles */
button.active {
background: #10b981;
}
/* ✅ Global styles with :global() */
:global(body) {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
</style>Let's build a complete weather dashboard application that demonstrates all Svelte fundamentals.
weather-dashboard/
├── src/
│ ├── components/
│ │ ├── WeatherCard.svelte
│ │ ├── SearchBar.svelte
│ │ ├── WeatherList.svelte
│ │ └── WeatherFilter.svelte
│ ├── stores/
│ │ └── weatherStore.ts
│ ├── types/
│ │ └── weather.ts
│ ├── App.svelte
│ └── App.css
├── package.json
└── vite.config.tsexport interface Weather {
id: string;
city: string;
country: string;
temperature: number;
feelsLike: number;
humidity: number;
windSpeed: number;
description: string;
icon: string;
timestamp: Date;
}
export type TemperatureUnit = 'celsius' | 'fahrenheit';
export type SortType = 'name' | 'temperature' | 'recent';import { writable, derived } from 'svelte/store';
import type { Weather, TemperatureUnit, SortType } from '../types/weather';
// ✅ Writable store for weather data
export const weatherList = writable<Weather[]>([]);
// ✅ Writable store for temperature unit
export const temperatureUnit = writable<TemperatureUnit>('celsius');
// ✅ Writable store for sort type
export const sortType = writable<SortType>('recent');
// ✅ Derived store for sorted and filtered weather
export const sortedWeather = derived(
[weatherList, sortType],
([$weatherList, $sortType]) => {
const sorted = [...$weatherList];
switch ($sortType) {
case 'name':
return sorted.sort((a, b) => a.city.localeCompare(b.city));
case 'temperature':
return sorted.sort((a, b) => b.temperature - a.temperature);
case 'recent':
default:
return sorted.sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
}
}
);
// ✅ Derived store for weather statistics
export const weatherStats = derived(
weatherList,
($weatherList) => {
if ($weatherList.length === 0) {
return { total: 0, avgTemp: 0, hottest: null, coldest: null };
}
const temps = $weatherList.map(w => w.temperature);
const avgTemp = temps.reduce((a, b) => a + b, 0) / temps.length;
const hottest = $weatherList.reduce((a, b) =>
a.temperature > b.temperature ? a : b
);
const coldest = $weatherList.reduce((a, b) =>
a.temperature < b.temperature ? a : b
);
return {
total: $weatherList.length,
avgTemp: Math.round(avgTemp),
hottest,
coldest
};
}
);
// ✅ Store functions
export function addWeather(weather: Weather) {
weatherList.update(list => [weather, ...list]);
}
export function removeWeather(id: string) {
weatherList.update(list => list.filter(w => w.id !== id));
}
export function updateWeather(id: string, updates: Partial<Weather>) {
weatherList.update(list =>
list.map(w => w.id === id ? { ...w, ...updates } : w)
);
}
export function clearWeather() {
weatherList.set([]);
}<script lang="ts">
import { temperatureUnit } from '../stores/weatherStore';
import type { Weather } from '../types/weather';
export let weather: Weather;
export let onDelete: (id: string) => void;
let isExpanded = false;
$: displayTemp = $temperatureUnit === 'fahrenheit'
? Math.round((weather.temperature * 9/5) + 32)
: weather.temperature;
$: feelsLikeTemp = $temperatureUnit === 'fahrenheit'
? Math.round((weather.feelsLike * 9/5) + 32)
: weather.feelsLike;
const tempUnit = $temperatureUnit === 'fahrenheit' ? '°F' : '°C';
</script>
<div class="card" class:expanded={isExpanded}>
<div class="card-header" on:click={() => isExpanded = !isExpanded}>
<div class="location">
<h3>{weather.city}, {weather.country}</h3>
<p class="description">{weather.description}</p>
</div>
<div class="temperature">
<span class="value">{displayTemp}{tempUnit}</span>
<span class="icon">{weather.icon}</span>
</div>
</div>
{#if isExpanded}
<div class="card-details" transition:slide>
<div class="detail-row">
<span class="label">Feels Like:</span>
<span class="value">{feelsLikeTemp}{tempUnit}</span>
</div>
<div class="detail-row">
<span class="label">Humidity:</span>
<span class="value">{weather.humidity}%</span>
</div>
<div class="detail-row">
<span class="label">Wind Speed:</span>
<span class="value">{weather.windSpeed} m/s</span>
</div>
<div class="detail-row">
<span class="label">Updated:</span>
<span class="value">{new Date(weather.timestamp).toLocaleTimeString()}</span>
</div>
</div>
{/if}
<button class="delete-btn" on:click={() => onDelete(weather.id)}>
✕
</button>
</div>
<style>
.card {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.location h3 {
margin: 0;
font-size: 1.1rem;
color: #333;
}
.description {
margin: 4px 0 0 0;
font-size: 0.875rem;
color: #666;
text-transform: capitalize;
}
.temperature {
display: flex;
align-items: center;
gap: 12px;
}
.value {
font-size: 1.5rem;
font-weight: 600;
color: #667eea;
}
.icon {
font-size: 2rem;
}
.card-details {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e5e7eb;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 0.875rem;
}
.label {
color: #666;
}
.delete-btn {
position: absolute;
top: 8px;
right: 8px;
background: #ef4444;
color: white;
border: none;
border-radius: 50%;
width: 28px;
height: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
}
.card:hover .delete-btn {
opacity: 1;
}
.card {
position: relative;
}
</style><script lang="ts">
import { addWeather } from '../stores/weatherStore';
import type { Weather } from '../types/weather';
let searchInput = '';
let isLoading = false;
let error = '';
async function handleSearch() {
if (!searchInput.trim()) return;
isLoading = true;
error = '';
try {
// Mock API call - replace with real weather API
const mockWeather: Weather = {
id: Date.now().toString(),
city: searchInput,
country: 'Country',
temperature: Math.round(Math.random() * 30),
feelsLike: Math.round(Math.random() * 30),
humidity: Math.round(Math.random() * 100),
windSpeed: Math.round(Math.random() * 20),
description: 'Partly cloudy',
icon: '⛅',
timestamp: new Date()
};
addWeather(mockWeather);
searchInput = '';
} catch (err) {
error = 'Failed to fetch weather data';
} finally {
isLoading = false;
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
handleSearch();
}
}
</script>
<div class="search-bar">
<input
type="text"
bind:value={searchInput}
on:keydown={handleKeydown}
placeholder="Search for a city..."
disabled={isLoading}
/>
<button on:click={handleSearch} disabled={isLoading || !searchInput.trim()}>
{isLoading ? 'Loading...' : 'Search'}
</button>
{#if error}
<p class="error">{error}</p>
{/if}
</div>
<style>
.search-bar {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
input {
flex: 1;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s;
}
input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
button:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-2px);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error {
color: #ef4444;
font-size: 0.875rem;
margin-top: 8px;
}
</style><script lang="ts">
import { sortedWeather } from '../stores/weatherStore';
import WeatherCard from './WeatherCard.svelte';
export let onDelete: (id: string) => void;
</script>
<div class="weather-list">
{#if $sortedWeather.length === 0}
<div class="empty-state">
<p>No weather data yet. Search for a city to get started!</p>
</div>
{:else}
{#each $sortedWeather as weather (weather.id)}
<WeatherCard {weather} {onDelete} />
{/each}
{/if}
</div>
<style>
.weather-list {
flex: 1;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #9ca3af;
}
</style><script lang="ts">
import { temperatureUnit, sortType, weatherStats } from '../stores/weatherStore';
import type { TemperatureUnit, SortType } from '../types/weather';
</script>
<div class="filter-container">
<div class="filter-group">
<label>Temperature Unit:</label>
<select bind:value={$temperatureUnit}>
<option value="celsius">Celsius (°C)</option>
<option value="fahrenheit">Fahrenheit (°F)</option>
</select>
</div>
<div class="filter-group">
<label>Sort By:</label>
<select bind:value={$sortType}>
<option value="recent">Most Recent</option>
<option value="name">City Name</option>
<option value="temperature">Temperature</option>
</select>
</div>
<div class="stats">
<div class="stat">
<span class="label">Total:</span>
<span class="value">{$weatherStats.total}</span>
</div>
<div class="stat">
<span class="label">Avg Temp:</span>
<span class="value">{$weatherStats.avgTemp}°</span>
</div>
{#if $weatherStats.hottest}
<div class="stat">
<span class="label">Hottest:</span>
<span class="value">{$weatherStats.hottest.city}</span>
</div>
{/if}
</div>
</div>
<style>
.filter-container {
background: #f9fafb;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
label {
font-weight: 600;
color: #374151;
font-size: 0.875rem;
}
select {
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 0.875rem;
cursor: pointer;
}
.stats {
display: flex;
gap: 16px;
margin-left: auto;
}
.stat {
display: flex;
gap: 6px;
font-size: 0.875rem;
}
.label {
color: #6b7280;
}
.value {
font-weight: 600;
color: #667eea;
}
</style><script lang="ts">
import { removeWeather, clearWeather } from './stores/weatherStore';
import SearchBar from './components/SearchBar.svelte';
import WeatherList from './components/WeatherList.svelte';
import WeatherFilter from './components/WeatherFilter.svelte';
</script>
<div class="app">
<header class="app-header">
<h1>Weather Dashboard</h1>
<p>Track weather across multiple cities</p>
</header>
<main class="app-main">
<div class="container">
<SearchBar />
<WeatherFilter />
<WeatherList onDelete={removeWeather} />
<button class="clear-btn" on:click={clearWeather}>
Clear All
</button>
</div>
</main>
</div>
<style>
:global(body) {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
text-align: center;
color: white;
padding: 40px 20px;
}
.app-header h1 {
margin: 0;
font-size: 2.5rem;
}
.app-header p {
margin: 10px 0 0 0;
font-size: 1.1rem;
opacity: 0.9;
}
.app-main {
flex: 1;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
}
.clear-btn {
align-self: flex-end;
padding: 10px 20px;
background: #ef4444;
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-top: 20px;
}
.clear-btn:hover {
background: #dc2626;
transform: translateY(-2px);
}
@media (max-width: 640px) {
.app-header h1 {
font-size: 1.8rem;
}
.container {
padding: 20px;
}
}
</style>* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
width: 100%;
height: 100%;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Transitions */
@media (prefers-reduced-motion: no-preference) {
* {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
}// ✅ Use reactive declarations for derived state
$: doubled = count * 2;
// ✅ Use reactive blocks for side effects
$: if (count > 10) {
console.log('count exceeded 10');
}
// ❌ Avoid unnecessary reactivity
// Don't use $: for simple values that don't change
let staticValue = 'hello'; // No need for $:// ✅ Use stores for shared state
import { count } from './stores';
// ✅ Subscribe to stores with $ prefix
<p>{$count}</p>
// ✅ Use derived stores for computed values
export const doubled = derived(count, $count => $count * 2);
// ❌ Avoid storing UI state in stores
// Keep UI state local to components// ✅ Keep components focused and small
// ✅ Use meaningful prop names
// ✅ Provide default values for props
export let title = 'Default Title';
// ✅ Use slots for composition
<slot />
// ❌ Avoid prop drilling
// Use stores or context for deeply nested data// ✅ Use keyed each blocks for lists
{#each items as item (item.id)}
<Item {item} />
{/each}
// ✅ Use animations sparingly
// ✅ Lazy load components when possible
// ✅ Use onMount for expensive operations
// ❌ Avoid creating functions in templates
// ❌ Don't use index as key in each blocks// ✅ Group related logic together
// ✅ Extract reusable components
// ✅ Use TypeScript for type safety
// ✅ Keep styles scoped to components
// ✅ Use meaningful variable names// ❌ Wrong - won't be reactive
<p>{count}</p>
// ✅ Correct - reactive subscription
<p>{$count}</p>
// ✅ Or subscribe manually
<script>
import { count } from './stores';
let unsubscribe;
onMount(() => {
unsubscribe = count.subscribe(value => {
// handle value
});
return unsubscribe;
});
</script>// ❌ Wrong - mutation won't trigger reactivity
items.push(newItem);
// ✅ Correct - create new array
items = [...items, newItem];
// ✅ Or use array methods that return new arrays
items = items.filter(item => item.id !== id);
// ❌ Wrong - object mutation
user.name = 'John';
// ✅ Correct - create new object
user = { ...user, name: 'John' };// ❌ Wrong - can cause issues with reordering
{#each items as item, index (index)}
<Item {item} />
{/each}
// ✅ Correct - use unique identifier
{#each items as item (item.id)}
<Item {item} />
{/each}// ❌ Wrong - memory leak
onMount(() => {
const unsubscribe = store.subscribe(value => {
// handle value
});
// forgot to return cleanup
});
// ✅ Correct - return cleanup function
onMount(() => {
const unsubscribe = store.subscribe(value => {
// handle value
});
return unsubscribe;
});// ❌ Wrong - side effect in reactive declaration
$: {
fetch(`/api/user/${userId}`).then(r => r.json());
}
// ✅ Correct - use onMount or reactive block with proper handling
onMount(async () => {
const response = await fetch(`/api/user/${userId}`);
const data = await response.json();
});
// ✅ Or use reactive block with proper structure
$: if (userId) {
loadUser(userId);
}Svelte represents a fundamental shift in how we think about frontend frameworks. By moving work to compile time, Svelte delivers smaller bundles, better performance, and a more intuitive developer experience.
The weather dashboard application we built demonstrates all core Svelte concepts in action. Understanding reactive declarations, stores, components, and lifecycle hooks is essential for building scalable Svelte applications.
Key takeaways:
Next steps:
Svelte is a journey toward simpler, faster, and more enjoyable web development. Keep learning, building, and pushing the boundaries of what's possible.