17.7 Pro Tips
Advanced patterns and optimization techniques for using Svelte and SvelteKit effectively in real-world projects.
$lib Alias Path Usage
SvelteKit lets you reference the src/lib folder as $lib. This avoids deep relative paths and keeps code clean.
src/lib/
├── components/
│ ├── ui/
│ │ ├── Button.svelte
│ │ ├── Modal.svelte
│ │ └── Input.svelte
│ └── layout/
│ ├── Header.svelte
│ └── Footer.svelte
├── stores/
│ ├── auth.js
│ └── theme.js
├── utils/
│ ├── date.js
│ ├── format.js
│ └── validation.js
├── server/ # Server-only code (never exposed to client)
│ ├── db.js
│ └── email.js
└── types.d.ts # TypeScript type definitions
<!-- Use $lib instead of relative paths -->
<script>
// Not recommended
import Button from '../../../components/ui/Button.svelte';
import { formatDate } from '../../../../utils/date.js';
// Recommended
import Button from '$lib/components/ui/Button.svelte';
import { formatDate } from '$lib/utils/date.js';
</script>
Additional Alias Configuration
// svelte.config.js
export default {
kit: {
alias: {
$lib: 'src/lib',
$components: 'src/lib/components',
$stores: 'src/lib/stores',
$utils: 'src/lib/utils',
$types: 'src/lib/types.d.ts',
},
},
};
$props() Rune — Component Props
In Svelte 5, component props are declared with the $props() Rune.
<!-- Button.svelte -->
<script>
// Basic props declaration
let {
label = 'Click',
variant = 'primary',
size = 'medium',
disabled = false,
onclick,
} = $props();
const sizeClasses = {
small: 'btn-sm',
medium: 'btn-md',
large: 'btn-lg',
};
</script>
<button
class="btn btn-{variant} {sizeClasses[size]}"
{disabled}
{onclick}
>
{label}
</button>
Rest Props
<!-- Input.svelte -->
<script>
// Extract known props, pass rest to input element
let { label, value = $bindable(''), ...restProps } = $props();
</script>
<div class="field">
{#if label}
<label>{label}</label>
{/if}
<!-- ...restProps forwards placeholder, type, maxlength, etc. automatically -->
<input bind:value {...restProps} />
</div>
Props with TypeScript
<!-- Card.svelte -->
<script lang="ts">
interface Props {
title: string;
description?: string;
imageUrl?: string;
href?: string;
variant?: 'default' | 'featured' | 'compact';
onclick?: () => void;
}
let {
title,
description = '',
imageUrl,
href,
variant = 'default',
onclick,
}: Props = $props();
</script>
$bindable() — Two-Way Binding Props
Makes a prop bindable so the parent component can use the bind: directive.
<!-- TextInput.svelte -->
<script>
let { value = $bindable(''), placeholder = '', ...rest } = $props();
</script>
<input bind:value {placeholder} {...rest} />
<!-- Parent component -->
<script>
import TextInput from '$lib/components/TextInput.svelte';
let username = $state('');
let email = $state('');
</script>
<!-- Two-way binding with bind:value -->
<TextInput bind:value={username} placeholder="Username" />
<TextInput bind:value={email} placeholder="Email" type="email" />
<p>Input: {username} / {email}</p>
Checkbox Group Component
<!-- CheckboxGroup.svelte -->
<script>
let {
options,
selected = $bindable([]),
label = '',
} = $props();
</script>
<fieldset>
{#if label}<legend>{label}</legend>{/if}
{#each options as option}
<label>
<input
type="checkbox"
bind:group={selected}
value={option.value}
/>
{option.label}
</label>
{/each}
</fieldset>
Event Forwarding vs Callback Props
Event forwarding has been simplified in Svelte 5.
Svelte 4 Event Forwarding (Legacy)
<!-- Svelte 4 -->
<button on:click>Click</button> <!-- Auto-forward event -->
Svelte 5 Callback Props (Recommended)
<!-- ModalDialog.svelte -->
<script>
let { isOpen = false, onclose, onconfirm, title, children } = $props();
</script>
{#if isOpen}
<div class="modal-overlay" onclick={onclose}>
<div class="modal" onclick|stopPropagation>
<header>
<h2>{title}</h2>
<button onclick={onclose}>×</button>
</header>
<div class="body">
{@render children?.()}
</div>
<footer>
<button onclick={onclose}>Cancel</button>
<button onclick={onconfirm} class="primary">Confirm</button>
</footer>
</div>
</div>
{/if}
<!-- Parent -->
<script>
import ModalDialog from '$lib/components/ModalDialog.svelte';
let showModal = $state(false);
let result = $state('');
function handleConfirm() {
result = 'confirmed';
showModal = false;
}
</script>
<button onclick={() => showModal = true}>Open Modal</button>
<ModalDialog
isOpen={showModal}
title="Are you sure?"
onclose={() => showModal = false}
onconfirm={handleConfirm}
>
<p>This action cannot be undone.</p>
</ModalDialog>
Performance Optimization
\{#key} — Force Component Remount
<script>
let userId = $state(1);
</script>
<!-- UserProfile is fully recreated whenever userId changes -->
{#key userId}
<UserProfile {userId} />
{/key}
{@const} — In-Block Computation Optimization
{#each products as product}
{@const subtotal = product.price * product.quantity}
{@const discount = subtotal > 1000 ? subtotal * 0.1 : 0}
{@const final = subtotal - discount}
<div>
<span>{product.name}</span>
<span>${final.toFixed(2)}</span>
{#if discount > 0}
<span class="discount">(saved ${discount.toFixed(2)})</span>
{/if}
</div>
{/each}
Lazy Loading
<script>
import { onMount } from 'svelte';
let HeavyChart = $state(null);
onMount(async () => {
const module = await import('$lib/components/HeavyChart.svelte');
HeavyChart = module.default;
});
</script>
{#if HeavyChart}
<svelte:component this={HeavyChart} data={chartData} />
{:else}
<div class="skeleton-chart">Loading chart...</div>
{/if}
Svelte 5 Snippets (\{#snippet}) and Render Tags ({@render})
Snippets introduced in Svelte 5 define reusable markup fragments. They are an evolution of slots.
Basic Snippets
<!-- DataTable.svelte -->
<script>
let { data, columns, rowActions } = $props();
</script>
<table>
<thead>
<tr>
{#each columns as col}
<th>{col.label}</th>
{/each}
{#if rowActions}
<th>Actions</th>
{/if}
</tr>
</thead>
<tbody>
{#each data as row}
<tr>
{#each columns as col}
<td>{row[col.key]}</td>
{/each}
{#if rowActions}
<td>{@render rowActions(row)}</td>
{/if}
</tr>
{/each}
</tbody>
</table>
<!-- Usage -->
<script>
import DataTable from '$lib/components/DataTable.svelte';
import { goto } from '$app/navigation';
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' },
];
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
];
</script>
<DataTable data={users} {columns}>
{#snippet rowActions(row)}
<button onclick={() => goto(`/admin/users/${row.id}`)}>Edit</button>
<button onclick={() => deleteUser(row.id)} class="danger">Delete</button>
{/snippet}
</DataTable>
Reusing Snippets Within the Same File
<script>
let items = $state(['apple', 'banana', 'cherry']);
</script>
{#snippet item(text, index)}
<li class="item">
<span class="index">{index + 1}</span>
<span>{text}</span>
</li>
{/snippet}
<ul>
{#each items as text, i}
{@render item(text, i)}
{/each}
</ul>
Folder Structure Best Practices
src/
├── lib/
│ ├── components/
│ │ ├── ui/ # Atomic UI components (Button, Input, etc.)
│ │ ├── forms/ # Form-related components
│ │ ├── layout/ # Layout components (Header, Sidebar)
│ │ └── features/ # Feature-specific components
│ ├── stores/
│ │ ├── auth.svelte.js # Auth state
│ │ ├── cart.svelte.js # Shopping cart
│ │ └── ui.svelte.js # UI state (modals, toasts, etc.)
│ ├── utils/
│ │ ├── api.js # API client
│ │ ├── date.js # Date formatting
│ │ └── validation.js # Validation helpers
│ ├── server/ # Server-only (never exposed to client)
│ │ ├── db.js
│ │ └── auth.js
│ └── types/ # TypeScript types
│ └── index.d.ts
└── routes/
├── (marketing)/ # Marketing page group
│ ├── +layout.svelte
│ ├── +page.svelte
│ └── about/
├── (app)/ # App feature group (requires auth)
│ ├── +layout.server.js # Auth check
│ ├── dashboard/
│ └── settings/
└── api/ # API endpoints
└── v1/
Testing (Vitest + @testing-library/svelte)
Installation
npm install -D @testing-library/svelte @testing-library/jest-dom vitest jsdom
Test Setup
// vite.config.js
import { defineConfig } from 'vite';
import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}'],
globals: true,
environment: 'jsdom',
setupFiles: ['./src/setupTests.js'],
},
});
// src/setupTests.js
import '@testing-library/jest-dom';
Component Tests
// src/lib/components/Counter.test.js
import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Counter from './Counter.svelte';
describe('Counter component', () => {
it('renders initial value', () => {
render(Counter, { props: { initialCount: 5, label: 'Test' } });
expect(screen.getByText(/Test: 5/)).toBeInTheDocument();
});
it('increments on +1 button click', async () => {
render(Counter, { props: { initialCount: 0 } });
const button = screen.getByText('+1');
await fireEvent.click(button);
expect(screen.getByText(/1/)).toBeInTheDocument();
});
it('resets to initial value', async () => {
render(Counter, { props: { initialCount: 10 } });
const incrementBtn = screen.getByText('+1');
const resetBtn = screen.getByText('Reset');
await fireEvent.click(incrementBtn);
await fireEvent.click(resetBtn);
expect(screen.getByText(/10/)).toBeInTheDocument();
});
});
Store Tests
// src/lib/stores/counter.test.js
import { describe, it, expect } from 'vitest';
import { get } from 'svelte/store';
import { counter } from './counter.js';
describe('counter store', () => {
it('has correct initial value', () => {
expect(get(counter)).toBe(10);
});
it('increment works', () => {
counter.increment();
expect(get(counter)).toBe(11);
});
it('reset returns to initial value', () => {
counter.reset();
expect(get(counter)).toBe(10);
});
});
Svelte Inspector
Click a component during development to jump directly to its source file in VS Code.
// vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
export default {
plugins: [
sveltekit({
inspector: {
toggleKeyCombo: 'meta-shift',
showToggleButton: 'always',
toggleButtonPos: 'bottom-left',
},
}),
],
};
Deployment: Cloudflare Pages
# 1. Install adapter
npm install -D @sveltejs/adapter-cloudflare
# 2. Update svelte.config.js
# 3. Build
npm run build
# 4. Connect to Cloudflare Pages
# Link your GitHub repository to Cloudflare Pages for automatic deploys
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};
Cloudflare Pages Environment Variables
// src/routes/api/data/+server.js
export async function GET({ platform }) {
// Access KV, D1, R2 in Cloudflare environment
const data = await platform?.env?.MY_KV?.get('key');
return json({ data });
}
Deployment: Vercel
npm install -D @sveltejs/adapter-vercel
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';
export default {
kit: {
adapter: adapter({
runtime: 'edge', // Optional: use Edge Runtime
}),
},
};
Practical Example: Reusable Toast Notification System
// src/lib/stores/toast.svelte.js
let toasts = $state([]);
let idCounter = 0;
export function addToast(message, type = 'info', duration = 3000) {
const id = ++idCounter;
toasts.push({ id, message, type });
setTimeout(() => {
removeToast(id);
}, duration);
return id;
}
export function removeToast(id) {
const index = toasts.findIndex(t => t.id === id);
if (index !== -1) toasts.splice(index, 1);
}
export { toasts };
<!-- src/lib/components/ToastContainer.svelte -->
<script>
import { fly, fade } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { toasts, removeToast } from '$lib/stores/toast.svelte.js';
</script>
<div class="toast-container" aria-live="polite">
{#each toasts as toast (toast.id)}
<div
class="toast toast-{toast.type}"
role="alert"
animate:flip
in:fly={{ x: 300, duration: 300 }}
out:fade={{ duration: 200 }}
>
<span>{toast.message}</span>
<button onclick={() => removeToast(toast.id)} aria-label="Close">×</button>
</div>
{/each}
</div>
<style>
.toast-container {
position: fixed;
bottom: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 9999;
}
.toast {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 8px;
min-width: 280px;
max-width: 400px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
color: white;
}
.toast span { flex: 1; }
.toast button { background: none; border: none; color: white; cursor: pointer; opacity: 0.8; }
.toast-info { background: #3b82f6; }
.toast-success { background: #22c55e; }
.toast-warning { background: #f59e0b; }
.toast-error { background: #ef4444; }
</style>
<!-- Usage from any page -->
<script>
import { addToast } from '$lib/stores/toast.svelte.js';
</script>
<button onclick={() => addToast('Saved successfully!', 'success')}>Save</button>
<button onclick={() => addToast('An error occurred.', 'error', 5000)}>Test Error</button>
Svelte 5 Migration Checklist
When migrating from an existing Svelte 4 project to Svelte 5:
# Automatic migration tool
npx sv migrate svelte-5
| Svelte 4 | Svelte 5 | Notes |
|---|---|---|
let x = 0 (component top-level) | let x = $state(0) | In Runes files |
$: y = x * 2 | let y = $derived(x * 2) | |
$: { sideEffect() } | $effect(() => { sideEffect() }) | |
export let name | let { name } = $props() | |
on:click={fn} | onclick={fn} | HTML event attributes |
<slot> | {@render children()} | Snippet-based |
createEventDispatcher | Callback props |
Pro Tips Collection
Tip 1: Reactive Media Queries
// src/lib/stores/mediaQuery.svelte.js
function createMediaQuery(query) {
let matches = $state(false);
if (typeof window !== 'undefined') {
const mq = window.matchMedia(query);
matches = mq.matches;
mq.addEventListener('change', (e) => { matches = e.matches; });
}
return { get matches() { return matches; } };
}
export const isMobile = createMediaQuery('(max-width: 768px)');
export const isDark = createMediaQuery('(prefers-color-scheme: dark)');
Tip 2: Form Validation Library Integration
npm install sveltekit-superforms zod
<script>
import { superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters.'),
email: z.string().email('Please enter a valid email.'),
});
const { form, errors, enhance } = superForm(data.form, {
validators: zodClient(schema),
});
</script>
<form use:enhance method="POST">
<input bind:value={$form.name} name="name" />
{#if $errors.name}<p>{$errors.name}</p>{/if}
<input bind:value={$form.email} name="email" type="email" />
{#if $errors.email}<p>{$errors.email}</p>{/if}
<button>Submit</button>
</form>
Tip 3: Leverage Built-in Accessibility Warnings
The Svelte compiler warns about accessibility issues by default:
<!-- Don't ignore these warnings -->
<!-- a11y: <img> element should have an alt attribute -->
<img src="..." />
<!-- Correct usage -->
<img src="..." alt="Product image" />
<!-- When you intentionally need to suppress a warning -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div onclick={handler}>...</div>
Summary
| Tip | Key Takeaway |
|---|---|
$lib alias | Clean import paths |
$props() | Explicit prop declaration |
$bindable() | Allow two-way binding |
| Callback props | Clearer than event forwarding |
{#snippet} | Reusable markup fragments |
{@render} | Call a snippet |
.svelte.js | Runes outside components |
| Adapter choice | Match to deployment environment |
| Vitest | Fast unit/component testing |
You've mastered Svelte 5 and SvelteKit! Next, explore Solid.js (Ch 18) or Qwik (Ch 19).