This content originally appeared on DEV Community and was authored by Ali Aslam
Alright, so we’ve already built some pretty cool stuff—powerful stores, actions, and flexible components that talk to each other. But we’re not done yet! Two things are still missing from our toolbox:
Lifecycle hooks — controlling when your code runs, and cleaning up when components disappear.
Accessibility (a11y) — ensuring your components aren’t just shiny, but also usable for all users.
This article covers both: minimal, powerful lifecycle hooks and practical accessibility patterns you can use immediately.
Part A — Lifecycle Hooks
If you’ve used other frameworks:
- In React you’d reach for
useEffect
. - In Vue there are hooks like
mounted
andbeforeUnmount
.
In Svelte, lifecycle hooks are minimal but powerful — just what you need, no boilerplate.
onMount
Runs after the component first renders in the DOM.
Think of it like Svelte saying:
“Okay, your component is finally on the page — now’s your chance to measure things, fetch data, or wire up events.”
Great for:
- Fetching data
- Measuring DOM nodes
- Adding event listeners
<!-- src/routes/+page.svelte -->
<script>
import { onMount } from 'svelte';
let width;
onMount(() => {
width = window.innerWidth;
const handleResize = () => (width = window.innerWidth);
window.addEventListener('resize', handleResize);
// cleanup when unmounting
return () => window.removeEventListener('resize', handleResize);
});
</script>
<p>Window width: {width}</p>
Without cleanup, you’d leave behind stray resize listeners every time this component mounted/unmounted — a slow memory leak.
$effect.pre
(before DOM update)
Runs just before Svelte updates the DOM when reactive state changes.
It’s like Svelte tapping you on the shoulder:
“Heads up, I’m about to change the DOM — want to look at the old value first?”
<!-- src/routes/+page.svelte -->
<script>
let count = 0;
$effect.pre(() => {
console.log("Before update: count =", count);
});
$effect(() => {
console.log("After update: count =", count);
});
</script>
<button onclick={() => count++}>+1</button>
Use $effect.pre when you wanna sneak a peek at the old value before Svelte changes everything. Think of it like getting a look at the ‘before’ picture in a makeover, right before the big transformation!.
Don’t try to edit the DOM here — Svelte will overwrite your changes immediately.
$effect
(after DOM update)
Runs right after the DOM has been updated.
This is your place to interact with the final DOM: measuring, scrolling, or syncing with third-party libraries.
<!-- src/routes/+page.svelte -->
<script>
let messages = [];
function addMessage() {
messages = [...messages, `Message ${messages.length + 1}`];
}
$effect(() => {
const list = document.querySelector('#messages');
if (list) list.scrollTop = list.scrollHeight;
});
</script>
<ul id="messages" style="height:100px; overflow-y:auto;">
{#each messages as msg}
<li>{msg}</li>
{/each}
</ul>
<button onclick={addMessage}>Add message</button>
Perfect for chat apps: each new message scrolls into view automatically.
Think of $effect
as your “after the dust settles” hook.
onDestroy
Runs when a component is removed from the DOM.
Think of it like cleaning up your campsite before you leave — no timers, no listeners left behind.
<!-- src/routes/+page.svelte -->
<script>
import { onDestroy } from 'svelte';
let timer = setInterval(() => console.log('tick'), 1000);
onDestroy(() => {
clearInterval(timer);
});
</script>
<p>Watch the console. When I unmount, timer stops.</p>
Forgetting cleanup here is like leaving your laundry out and never picking it up—eventually, it piles up and starts to smell! You don’t want those stray listeners or timers causing your app to slow down or crash.
Mini Recap
Lifecycle hooks give you a clean way to manage setup, updates, and teardown:
-
onMount
→ run setup logic once the DOM is ready -
$effect.pre
→ peek at values before DOM changes -
$effect
→ react after DOM changes -
onDestroy
→ cleanup when leaving
They’re simple, predictable, and cover most real-world needs without extra libraries.
Part B — Accessibility (a11y)
Accessibility isn’t a “nice to have”, it’s a must-have! If your app looks sleek but can’t be used with a keyboard, a screen reader, or by someone with low vision, then for those users…it’s broken.
The good news: Svelte already helps by warning about some a11y issues in the compiler. The rest is up to us — adding semantics, handling focus, and making sure our colors and interactions are usable by everyone.
ARIA Roles
ARIA roles are like name tags at a party. Without them, assistive tech sees a <div>
and goes, “uhh… what are you?”
Example: modal dialog.
<!-- src/routes/+page.svelte -->
<div role="dialog" aria-labelledby="title" aria-modal="true">
<h2 id="title">Confirm Delete</h2>
<button>Cancel</button>
<button>Delete</button>
</div>
Now screen readers know: “This is a dialog with a title.” Not just a random
<div>
.
Keyboard Navigation
Everything should work without a mouse. Menus, tabs, dialogs — they all need keyboard support.
<!-- src/routes/+page.svelte -->
<script>
let items = ['Home', 'About', 'Contact'];
let current = 0;
function handleKey(e) {
if (e.key === 'ArrowDown') current = (current + 1) % items.length;
if (e.key === 'ArrowUp') current = (current - 1 + items.length) % items.length;
}
</script>
<ul tabindex="0" onkeydown={handleKey}>
{#each items as item, i}
<li class:active={i === current}>{item}</li>
{/each}
</ul>
<style>
li.active {
background: #ddd;
}
</style>
Try the arrow keys — focus moves up and down the list like you’d expect.
Focus Management
When a modal opens, focus should jump into it. Otherwise, keyboard users can get stuck “behind the curtain.”
<!-- src/routes/+page.svelte -->
<script>
import { onMount } from 'svelte';
let modal;
onMount(() => {
modal.querySelector('button').focus();
});
</script>
<div role="dialog" bind:this={modal}>
<h2>Welcome</h2>
<button>Close</button>
</div>
Now, the focus jumps straight to the button like a spotlight on stage! No more awkwardly tabbing through the background—your keyboard users will love you for this.
Labels & Inputs
Inputs need proper labels — otherwise screen readers just announce “edit text” with no context.
<!-- Using "for" / "id" -->
<label for="email">Email</label>
<input id="email" type="email" />
Or the wrapping style:
<label>
Email
<input type="email" />
</label>
Both are fully screen-reader friendly.
Color Contrast
Designers love light grays, but your eyes (and your users’) don’t.
Use the WebAIM contrast checker to make sure text is readable.
Aim for at least 4.5:1 contrast ratio for normal text.
Live Regions
When something changes in your app (like a new message or notification), you want to make sure your users know about it, right? That’s where aria-live comes in. It’s like having an invisible helper announcing changes, so no one misses out!
<!-- src/routes/+page.svelte -->
<script>
let message = "Ready.";
</script>
<p aria-live="polite">{message}</p>
<button onclick={() => message = "Saved!"}>Save</button>
When you click “Save,” screen readers will literally speak the word “Saved!”
Real-World Example: Accessible Tabs
Let’s build a set of tabs that play nicely with both keyboards and screen readers.
<!-- src/lib/components/Tabs.svelte -->
<script>
export let tabs = [];
export let children;
let active = 0;
</script>
<div role="tablist">
{#each tabs as tab, i}
<button
role="tab"
aria-selected={i === active}
aria-controls={"panel-" + i}
id={"tab-" + i}
onclick={() => (active = i)}
>
{tab.label}
</button>
{/each}
</div>
{#each tabs as tab, i}
<div
id={"panel-" + i}
role="tabpanel"
aria-labelledby={"tab-" + i}
hidden={i !== active}
>
{@render children?.({ name: tab.name })}
</div>
{/each}
Usage
<!-- src/routes/+page.svelte -->
<script>
import Tabs from '$lib/components/Tabs.svelte';
let tabs = [
{ label: 'One', name: 'one' },
{ label: 'Two', name: 'two' }
];
</script>
<Tabs {tabs}>
{({ name }) => {
if (name === 'one') return <div>Tab one content</div>;
if (name === 'two') return <div>Tab two content</div>;
}}
</Tabs>
Tabs announce which one is active, link each button to its panel, and properly hide inactive panels.
Accessibility is less about “extra features” and more about “basic usability for everyone.” With a few small habits — roles, labels, focus, contrast — your app becomes usable by all your users, not just some.
Accessible Modal (Mini Project) 
Let’s put everything together and build an accessible modal component.
It should:
- Announce itself properly with
role="dialog"
+aria-modal="true"
. - Automatically move focus inside when opened.
- Trap focus so tabbing doesn’t “escape” to the background.
- Close when you hit Escape.
Think of it like a pop-up window in real life:
- The curtain drops (backdrop),
- The spotlight shines inside (focus),
- And you can’t wander off until you close it.
Modal Component
<!-- src/lib/components/Modal.svelte -->
<script>
import { onMount } from 'svelte';
export let open = false;
let modal;
function close() {
open = false;
}
function handleKey(e) {
if (e.key === 'Escape') close();
// Basic focus trap
if (e.key === 'Tab' && modal) {
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
onMount(() => {
if (open) {
modal.querySelector('button')?.focus();
document.addEventListener('keydown', handleKey);
}
return () => {
document.removeEventListener('keydown', handleKey);
};
});
</script>
{#if open}
<div class="backdrop">
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
bind:this={modal}
>
<h2 id="modal-title">Confirm Action</h2>
<button onclick={close}>Cancel</button>
<button onclick={close}>Confirm</button>
</div>
</div>
{/if}
<style>
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
[role="dialog"] {
background: white;
padding: 2rem;
border-radius: 8px;
min-width: 300px;
}
button {
margin: 0.5rem;
}
</style>
Using the Modal
<!-- src/routes/+page.svelte -->
<script>
import Modal from '$lib/components/Modal.svelte';
let show = false;
</script>
<button onclick={() => (show = true)}>Open Modal</button>
<Modal bind:open={show} />
Why this is accessible
Semantic roles:
role="dialog"
+ aria-modal="true"
tell assistive tech this is a modal, not just another <div>
.
Focus management: When the modal opens, focus jumps straight to the first button. Users don’t get lost in the background.
Focus trap: Pressing Tab cycles between buttons inside the modal — you can’t tab into the page behind it.
Escape key: Hitting Escape closes the modal, no mouse required.
This little modal combines:
-
Lifecycle hooks (
onMount
) for setup and cleanup, - Keyboard handling for Escape/Tab,
- Accessibility best practices for roles, focus, and labels.
It’s a real-world component that feels good to use whether you’re on a mouse, keyboard, or screen reader.
With these patterns, you’re officially building like a pro.
Next in the series: Performance & Scaling — optimizing reactivity, code splitting, lazy loading, and testing.
Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Checkout my offering on YouTube with (growing) crash courses and content on JavaScript, React, TypeScript, Rust, WebAssembly, AI Prompt Engineering and more: @LearnAwesome
This content originally appeared on DEV Community and was authored by Ali Aslam