Skip to main content
Advertisement

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 -->
<!-- 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 4Svelte 5Notes
let x = 0 (component top-level)let x = $state(0)In Runes files
$: y = x * 2let y = $derived(x * 2)
$: { sideEffect() }$effect(() => { sideEffect() })
export let namelet { name } = $props()
on:click={fn}onclick={fn}HTML event attributes
<slot>{@render children()}Snippet-based
createEventDispatcherCallback 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

TipKey Takeaway
$lib aliasClean import paths
$props()Explicit prop declaration
$bindable()Allow two-way binding
Callback propsClearer than event forwarding
{#snippet}Reusable markup fragments
{@render}Call a snippet
.svelte.jsRunes outside components
Adapter choiceMatch to deployment environment
VitestFast unit/component testing

You've mastered Svelte 5 and SvelteKit! Next, explore Solid.js (Ch 18) or Qwik (Ch 19).

Advertisement