| name | svelte |
| description | Modern Svelte development for reactive web apps. Use when building Svelte components, managing state with stores, implementing real-time updates via WebSocket, or migrating from vanilla JS. Covers SvelteKit, TypeScript, and integration with Node.js backends. |
Svelte Skill
Overview
This skill provides expertise for building reactive web applications with Svelte. It covers component architecture, the reactivity system, stores for state management, real-time updates with WebSockets, and SvelteKit for full-stack applications.
Why Svelte
Comparison with Vanilla JS
| Aspect | Vanilla JS | Svelte |
|---|---|---|
| Reactivity | Manual DOM updates | Automatic - count++ just works |
| Components | Template strings | Single-file components |
| State | Global variables | Stores with subscriptions |
| Bundle size | 0kb (but more code) | ~2kb runtime |
| Learning curve | None | Gentle (closest to vanilla) |
Key Benefits
- Compile-time magic - No virtual DOM, compiles to efficient vanilla JS
- Less boilerplate -
let count = 0is reactive by default - Built-in transitions -
transition:fadefor animations - Scoped CSS - Styles in components don't leak
- Stores - Simple reactive state that works with WebSockets
Core Concepts
Reactivity
Svelte's reactivity is based on assignments:
<script>
let count = 0;
// Reactive statements run when dependencies change
$: doubled = count * 2;
$: console.log('count changed to', count);
function increment() {
count++; // This triggers UI update automatically
}
</script>
<button on:click={increment}>
Count: {count} (doubled: {doubled})
</button>
Array/Object Reactivity
Svelte tracks assignments, not mutations:
<script>
let items = ['a', 'b', 'c'];
// BAD: mutation doesn't trigger update
function addBad() {
items.push('d'); // UI won't update!
}
// GOOD: reassignment triggers update
function addGood() {
items = [...items, 'd']; // UI updates
}
// Also works: assign back to self
function addAlso() {
items.push('d');
items = items; // Triggers update
}
</script>
Component Structure
Single-file components with script, markup, and style:
<!-- PlayerCard.svelte -->
<script>
// Props with defaults
export let name;
export let cash = 0;
export let isActive = false;
// Local state
let expanded = false;
// Event dispatcher
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function handleClick() {
dispatch('select', { name });
}
</script>
<div class="player-card" class:active={isActive} on:click={handleClick}>
<h3>{name}</h3>
<p>Cash: £{cash}</p>
{#if expanded}
<slot /> <!-- Nested content goes here -->
{/if}
</div>
<style>
/* Scoped to this component only */
.player-card {
padding: 1rem;
border: 2px solid #333;
border-radius: 8px;
}
.player-card.active {
border-color: #4a9eff;
background: rgba(74, 158, 255, 0.1);
}
</style>
Using Components
<!-- Game.svelte -->
<script>
import PlayerCard from './PlayerCard.svelte';
let players = [
{ id: 1, name: 'Germany', cash: 15 },
{ id: 2, name: 'Britain', cash: 12 }
];
let activePlayerId = 1;
function handleSelect(event) {
console.log('Selected:', event.detail.name);
}
</script>
{#each players as player (player.id)}
<PlayerCard
name={player.name}
cash={player.cash}
isActive={player.id === activePlayerId}
on:select={handleSelect}
>
<p>Ships: {player.ships?.length ?? 0}</p>
</PlayerCard>
{/each}
Stores
Writable Stores
For shared state across components:
// stores/gameState.js
import { writable, derived } from 'svelte/store';
// Create a writable store
export const gameState = writable(null);
// Derived stores compute from other stores
export const currentPlayer = derived(
gameState,
$state => $state?.players?.[$state?.currentPlayerIndex]
);
export const isMyTurn = derived(
[gameState, currentPlayer],
([$state, $player]) => $player?.id === myPlayerId
);
// Helper functions to update state
export function updateGameState(newState) {
gameState.set(newState);
}
export function updatePlayer(playerId, changes) {
gameState.update(state => ({
...state,
players: {
...state.players,
[playerId]: { ...state.players[playerId], ...changes }
}
}));
}
Using Stores in Components
<script>
import { gameState, currentPlayer, isMyTurn } from './stores/gameState.js';
// $ prefix auto-subscribes to store
$: console.log('Game state updated:', $gameState);
</script>
<div>
<h2>Turn: {$gameState?.turn}</h2>
<p>Current player: {$currentPlayer?.name}</p>
{#if $isMyTurn}
<button>Take Action</button>
{:else}
<p>Waiting for {$currentPlayer?.name}...</p>
{/if}
</div>
Custom Stores
Create stores with custom methods:
// stores/player.js
import { writable } from 'svelte/store';
function createPlayerStore() {
const { subscribe, set, update } = writable({
cash: 0,
officers: 0,
engineers: 0,
gasCubes: { hydrogen: 0, helium: 0 }
});
return {
subscribe,
set,
reset: () => set({ cash: 0, officers: 0, engineers: 0, gasCubes: { hydrogen: 0, helium: 0 } }),
addCash: (amount) => update(p => ({ ...p, cash: p.cash + amount })),
spendCash: (amount) => update(p => ({ ...p, cash: p.cash - amount })),
buyGas: (type, amount) => update(p => ({
...p,
gasCubes: { ...p.gasCubes, [type]: p.gasCubes[type] + amount }
}))
};
}
export const player = createPlayerStore();
Real-Time Updates with WebSocket
Socket Store Pattern
// stores/socket.js
import { writable, get } from 'svelte/store';
import { io } from 'socket.io-client';
import { gameState } from './gameState.js';
export const connected = writable(false);
export const connectionError = writable(null);
let socket = null;
export function connect(serverUrl) {
socket = io(serverUrl, {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000
});
socket.on('connect', () => {
connected.set(true);
connectionError.set(null);
console.log('Connected to server');
});
socket.on('disconnect', () => {
connected.set(false);
});
socket.on('connect_error', (error) => {
connectionError.set(error.message);
});
// Game state updates from server
socket.on('state-update', (newState) => {
gameState.set(newState);
});
socket.on('state-sync', (fullState) => {
gameState.set(fullState);
});
return socket;
}
export function joinGame(gameId, playerId) {
if (socket) {
socket.emit('join-game', { gameId, playerId });
}
}
export function sendAction(action) {
if (socket) {
socket.emit('game-action', action);
}
}
export function disconnect() {
if (socket) {
socket.disconnect();
socket = null;
connected.set(false);
}
}
Using Socket in Components
<!-- Game.svelte -->
<script>
import { onMount, onDestroy } from 'svelte';
import { connect, joinGame, sendAction, disconnect, connected } from './stores/socket.js';
import { gameState, currentPlayer } from './stores/gameState.js';
export let gameId;
export let playerId;
onMount(() => {
connect('http://localhost:3000');
joinGame(gameId, playerId);
});
onDestroy(() => {
disconnect();
});
function handleEndTurn() {
sendAction({ type: 'END_TURN' });
}
</script>
{#if !$connected}
<div class="connecting">Connecting to server...</div>
{:else if !$gameState}
<div class="loading">Loading game state...</div>
{:else}
<div class="game">
<h1>Turn {$gameState.turn}</h1>
<p>Current player: {$currentPlayer?.name}</p>
<button on:click={handleEndTurn}>End Turn</button>
</div>
{/if}
Conditional Rendering and Loops
If/Else Blocks
{#if loading}
<Spinner />
{:else if error}
<ErrorMessage {error} />
{:else if items.length === 0}
<EmptyState />
{:else}
<ItemList {items} />
{/if}
Each Blocks with Keys
<!-- Key is crucial for list updates -->
{#each ships as ship (ship.id)}
<Ship {...ship} on:launch={handleLaunch} />
{:else}
<p>No ships in hangar</p>
{/each}
Await Blocks
{#await fetchGameState()}
<p>Loading...</p>
{:then state}
<GameBoard {state} />
{:catch error}
<p>Error: {error.message}</p>
{/await}
Transitions and Animations
Built-in Transitions
<script>
import { fade, fly, slide, scale } from 'svelte/transition';
import { flip } from 'svelte/animate';
let visible = true;
let items = [];
</script>
{#if visible}
<div transition:fade={{ duration: 300 }}>
Fades in and out
</div>
{/if}
<!-- One-way transitions -->
{#if showNotification}
<div in:fly={{ y: -50, duration: 300 }} out:fade>
Notification!
</div>
{/if}
<!-- Animate list reordering -->
{#each items as item (item.id)}
<div animate:flip={{ duration: 300 }}>
{item.name}
</div>
{/each}
Custom Transitions
<script>
function whoosh(node, { duration = 400 }) {
return {
duration,
css: (t) => {
const eased = t; // Could use easing function
return `
transform: scale(${eased}) rotate(${(1 - eased) * 360}deg);
opacity: ${eased};
`;
}
};
}
</script>
{#if show}
<div transition:whoosh>Whoooosh!</div>
{/if}
Event Handling
DOM Events
<button on:click={handleClick}>Click</button>
<button on:click={() => count++}>Inline</button>
<!-- Event modifiers -->
<button on:click|preventDefault={submit}>Submit</button>
<button on:click|stopPropagation={handleClick}>Stop Bubble</button>
<button on:click|once={init}>Initialize Once</button>
<form on:submit|preventDefault={handleSubmit}>...</form>
<!-- Keyboard events -->
<input on:keydown|self={(e) => e.key === 'Enter' && submit()} />
Component Events
<!-- Child component -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function handleSelect() {
dispatch('select', { id: item.id, name: item.name });
}
</script>
<!-- Parent component -->
<Card on:select={(e) => console.log(e.detail.name)} />
<!-- Forward DOM events -->
<button on:click>
This click bubbles to parent
</button>
Bindings
Two-Way Binding
<script>
let name = '';
let agreed = false;
let selected = 'a';
let quantity = 1;
</script>
<input bind:value={name} />
<input type="checkbox" bind:checked={agreed} />
<input type="number" bind:value={quantity} min="1" max="10" />
<select bind:value={selected}>
<option value="a">Option A</option>
<option value="b">Option B</option>
</select>
<!-- Group binding -->
<script>
let selectedColors = [];
</script>
{#each ['red', 'green', 'blue'] as color}
<label>
<input type="checkbox" bind:group={selectedColors} value={color} />
{color}
</label>
{/each}
Element Bindings
<script>
let inputElement;
let divWidth;
let divHeight;
</script>
<input bind:this={inputElement} />
<button on:click={() => inputElement.focus()}>Focus</button>
<div bind:clientWidth={divWidth} bind:clientHeight={divHeight}>
Size: {divWidth}x{divHeight}
</div>
SvelteKit
Project Structure
my-app/
├── src/
│ ├── lib/ # Shared components and utilities
│ │ ├── components/
│ │ │ ├── PlayerCard.svelte
│ │ │ └── GameBoard.svelte
│ │ ├── stores/
│ │ │ ├── gameState.js
│ │ │ └── socket.js
│ │ └── utils/
│ ├── routes/ # File-based routing
│ │ ├── +page.svelte # /
│ │ ├── +layout.svelte # Shared layout
│ │ ├── game/
│ │ │ ├── +page.svelte # /game
│ │ │ └── [id]/
│ │ │ └── +page.svelte # /game/:id
│ │ └── api/ # API routes
│ │ └── games/
│ │ └── +server.js
│ ├── app.html
│ └── app.css
├── static/ # Static assets
├── svelte.config.js
└── package.json
Page Load Functions
// routes/game/[id]/+page.js
export async function load({ params, fetch }) {
const response = await fetch(`/api/games/${params.id}`);
if (!response.ok) {
throw error(404, 'Game not found');
}
const game = await response.json();
return {
game,
gameId: params.id
};
}
<!-- routes/game/[id]/+page.svelte -->
<script>
export let data; // From load function
$: ({ game, gameId } = data);
</script>
<h1>Game: {game.name}</h1>
API Routes
// routes/api/games/+server.js
import { json } from '@sveltejs/kit';
export async function GET({ url }) {
const games = await db.getGames();
return json(games);
}
export async function POST({ request }) {
const { name, playerId } = await request.json();
const game = await db.createGame(name, playerId);
return json(game, { status: 201 });
}
TypeScript Support
<script lang="ts">
interface Player {
id: string;
name: string;
cash: number;
faction: 'germany' | 'britain' | 'usa' | 'italy';
}
interface Ship {
id: string;
name: string;
status: 'hangar' | 'on_route' | 'destroyed';
}
export let player: Player;
export let ships: Ship[] = [];
let selectedShip: Ship | null = null;
function selectShip(ship: Ship): void {
selectedShip = ship;
}
</script>
Migration from Vanilla JS
Before (Vanilla)
// Vanilla JS pattern
let gameState = null;
const stateElement = document.getElementById('game-state');
function render() {
stateElement.innerHTML = `
<h2>Turn ${gameState.turn}</h2>
<p>Cash: £${gameState.players[userId].cash}</p>
${gameState.players[userId].ships.map(ship => `
<div class="ship">${ship.name}</div>
`).join('')}
`;
}
async function fetchState() {
const res = await fetch(`/api/state/${gameId}`);
gameState = await res.json();
render();
}
// Poll every 2 seconds
setInterval(fetchState, 2000);
After (Svelte)
<script>
import { onMount } from 'svelte';
import { gameState } from './stores/gameState.js';
import { connect, joinGame } from './stores/socket.js';
export let gameId;
export let userId;
$: player = $gameState?.players?.[userId];
onMount(() => {
connect('http://localhost:3000');
joinGame(gameId, userId);
});
</script>
{#if $gameState}
<h2>Turn {$gameState.turn}</h2>
<p>Cash: £{player.cash}</p>
{#each player.ships as ship (ship.id)}
<div class="ship">{ship.name}</div>
{/each}
{:else}
<p>Loading...</p>
{/if}
Best Practices
Component Organization
lib/components/
├── ui/ # Generic reusable components
│ ├── Button.svelte
│ ├── Modal.svelte
│ └── Tooltip.svelte
├── game/ # Game-specific components
│ ├── GameBoard.svelte
│ ├── PlayerPanel.svelte
│ └── ShipCard.svelte
└── layout/ # Layout components
├── Header.svelte
└── Sidebar.svelte
Props and Events Naming
<script>
// Props: noun or adjective
export let player;
export let isActive = false;
export let maxItems = 10;
// Events: on:verbNoun pattern
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
// dispatch('select'), dispatch('launch'), dispatch('close')
</script>
<!-- Usage follows same pattern -->
<ShipCard
ship={myShip}
isSelected={selectedId === myShip.id}
on:launch={handleLaunch}
on:select={handleSelect}
/>
Reactive Statement Order
<script>
export let items;
export let filter;
// Derived values first (these update when deps change)
$: filteredItems = items.filter(i => i.type === filter);
$: totalCount = filteredItems.length;
// Side effects last (log, dispatch events, etc.)
$: if (totalCount === 0) {
console.log('No items match filter');
}
</script>
Avoiding Common Mistakes
<script>
// MISTAKE 1: Mutating without reassignment
let items = [1, 2, 3];
items.push(4); // Won't trigger update!
items = [...items, 4]; // Correct
// MISTAKE 2: Destructuring props loses reactivity
export let player;
const { name } = player; // name won't update!
$: ({ name } = player); // Reactive destructure
// MISTAKE 3: Not using key in each
{#each items as item} // Bad for updates
{#each items as item (item.id)} // Good
// MISTAKE 4: Store in template without $
import { count } from './stores';
// {count} shows store object, not value
// {$count} shows the value
</script>
Testing Svelte Components
// PlayerCard.test.js
import { render, fireEvent } from '@testing-library/svelte';
import PlayerCard from './PlayerCard.svelte';
describe('PlayerCard', () => {
it('displays player name and cash', () => {
const { getByText } = render(PlayerCard, {
props: { name: 'Germany', cash: 15 }
});
expect(getByText('Germany')).toBeInTheDocument();
expect(getByText('Cash: £15')).toBeInTheDocument();
});
it('dispatches select event on click', async () => {
const { getByRole, component } = render(PlayerCard, {
props: { name: 'Germany', cash: 15 }
});
const selectHandler = vi.fn();
component.$on('select', selectHandler);
await fireEvent.click(getByRole('button'));
expect(selectHandler).toHaveBeenCalledWith(
expect.objectContaining({
detail: { name: 'Germany' }
})
);
});
});
When This Skill Activates
Use this skill when:
- Building Svelte components
- Managing state with Svelte stores
- Implementing real-time updates via WebSocket
- Migrating vanilla JS to Svelte
- Setting up SvelteKit projects
- Adding TypeScript to Svelte
- Creating reactive UI patterns
- Optimizing Svelte performance