In Svelte, Actions and Children are powerful tools that make your components more dynamic and reusable. Whether you're adding behaviors like focus management, handling resizing, or giving parents the ability to inject custom content, these features are essential for building interactive and flexible UIs.
This guide will show you how to leverage Actions to add reusable DOM behaviors and Children to make your components more adaptable. By the end, you'll be able to create components that feel like LEGO blocks—small, composable, and easy to work with.
Why Actions? 🎬
We’ve seen how stores help with state management.
But what about reusing behavior on DOM elements?
While state management (via stores) helps us manage data, actions focus on controlling the behavior of DOM elements. They let us add reusable behaviors, like autofocus, clicks, resizing, and much more, making our components more flexible and powerful.
Stores vs Actions
- Stores → centralize and share state
- Actions → centralize and reuse DOM behavior
The problem
Imagine you want multiple inputs to autofocus when they appear.
Without actions, you’d write the same onMount
logic in every component:
<script>
import { onMount } from 'svelte';
let input;
onMount(() => input.focus());
</script>
<input bind:this={input}>
It works, but it’s copy-pasted boilerplate. 😩
The idea
With an action, you write the focus logic once and then attach it to any element using the use:
directive.
Think of actions as hooks for DOM nodes — little reusable plugins you can stick on elements.
Your First Action: Autofocus 👀
Let’s turn that autofocus behavior into an action.
Action file
// src/lib/actions/focus.js
export function autofocus(node) {
node.focus();
}
Usage in a page
<!-- src/routes/+page.svelte -->
<script>
import { autofocus } from '$lib/actions/focus.js';
</script>
<input placeholder="Type here..." use:autofocus />
How it works
- When the
<input>
mounts, Svelte calls ourautofocus
function with the DOM node. - The action immediately runs
node.focus()
. - Result: the input gets focus automatically, no
onMount
needed in your component.
✅ You can now sprinkle use:autofocus
anywhere without repeating logic. The use: directive is the key to applying actions to DOM elements. It tells Svelte to run the action on the DOM element when it mounts, providing a simple and reusable way to handle behaviors.
Actions with Cleanup 🧹
Some DOM behaviors need to attach event listeners (like click
, keydown
, or scroll
).
But here’s the catch: if you don’t remove those listeners when the element goes away, you’ll create memory leaks.
Actions solve this neatly because they can return an object with special lifecycle methods:
-
destroy()
→ runs when the element is removed from the DOM -
update(params)
→ runs if the parameter to the action changes (we’ll cover this in the next step)
Example: Click Outside Detector
Let’s make an action that closes a modal or dropdown when you click outside of it.
// src/lib/actions/clickOutside.js
export function clickOutside(node, callback) {
function handle(event) {
if (!node.contains(event.target)) {
callback();
}
}
document.addEventListener('click', handle, true);
return {
destroy() {
document.removeEventListener('click', handle, true);
}
};
}
Usage in a component
<!-- src/routes/+page.svelte -->
<script>
import { clickOutside } from '$lib/actions/clickOutside.js';
let open = true;
</script>
{#if open}
<div use:clickOutside={() => (open = false)}>
Click outside me to close.
</div>
{/if}
Why this works
- When the
<div>
mounts, the action attaches aclick
listener ondocument
. - If you click anywhere outside the
<div>
, the callback runs and setsopen = false
. - When the
<div>
is removed, thedestroy()
method is called, which cleans up the event listener. This cleanup is essential because, without it, event listeners would continue to exist even after the element is removed from the DOM, leading to unnecessary memory consumption and potential performance issues
✅ This is perfect for modals, dropdowns, sidebars, or tooltips that should close when you click elsewhere.
⚠️ Without the destroy()
cleanup, every new modal would add another document.addEventListener
— piling up over time, slowing down your app, and causing weird bugs.
Actions with Parameters 🎛️
Actions don’t just run once — they can also react to changing arguments.
When you write use:myAction={value}
, Svelte does three things under the hood:
- Calls the action when the element mounts →
myAction(node, value)
- Calls
update(newValue)
whenever the parameter changes - Calls
destroy()
when the element is removed
This makes actions really flexible — they can adapt as your component’s state changes. By passing parameters to actions, we can make them highly dynamic and reusable. For example, resizing a box based on a slider or updating styles based on a value changes without needing to repeat logic in each component.
Example: Resize Box
Here’s an action that resizes a box based on a numeric parameter.
// src/lib/actions/resize.js
export function resize(node, size) {
function apply(size) {
node.style.width = size + 'px';
node.style.height = size + 'px';
}
// run once on mount
apply(size);
return {
update(newSize) {
apply(newSize); // run whenever `size` changes
},
destroy() {
// cleanup: reset styles
node.style.width = '';
node.style.height = '';
}
};
}
Usage in a page
<!-- src/routes/+page.svelte -->
<script>
import { resize } from '$lib/actions/resize.js';
let boxSize = 100;
</script>
<div use:resize={boxSize} style="background: coral;"></div>
<input type="range" min="50" max="300" bind:value={boxSize} />
How it works
- On mount, the action sets the
<div>
’s width and height toboxSize
(100px to start). - As you move the slider,
boxSize
changes → Svelte calls the action’supdate()
→ the box resizes in real time. - When the
<div>
is removed,destroy()
resets its styles.
✅ The result: a resizable box that updates instantly as the slider moves.
Advanced Actions (Extra Examples) 🚀
By now you’ve seen how actions can add reusable behavior to DOM elements.
Let’s bring back some extra goodies from the old draft — actions that are a bit more advanced but super useful in real apps.
1. Debounced Input ⏳
Throttle keystrokes before firing off expensive work (like API requests).
// src/lib/actions/debounce.js
export function debounce(node, delay = 300) {
let timeout;
function handler(e) {
clearTimeout(timeout);
timeout = setTimeout(() => {
node.dispatchEvent(
new CustomEvent('debounced', { detail: e.target.value })
);
}, delay);
}
node.addEventListener('input', handler);
return {
destroy() {
node.removeEventListener('input', handler);
}
};
}
Usage
<!-- src/routes/+page.svelte -->
<script>
import { debounce } from '$lib/actions/debounce.js';
let search = '';
</script>
<input
use:debounce={500}
on:debounced={(e) => (search = e.detail)}
placeholder="Search..."
/>
<p>Search query: {search}</p>
✅ Super handy for search boxes — you don’t want to hammer your backend on every single keystroke. Debouncing helps optimize user experience by reducing unnecessary API calls, especially when dealing with search inputs, live validation, or any component that reacts to user typing.
2. Drag and Drop (Basic) 🖱️
Make elements draggable with almost no code.
// src/lib/actions/draggable.js
export function draggable(node) {
node.draggable = true;
function handleDragStart(e) {
e.dataTransfer.setData('text/plain', node.id);
node.classList.add('dragging');
}
function handleDragEnd() {
node.classList.remove('dragging');
}
node.addEventListener('dragstart', handleDragStart);
node.addEventListener('dragend', handleDragEnd);
return {
destroy() {
node.removeEventListener('dragstart', handleDragStart);
node.removeEventListener('dragend', handleDragEnd);
}
};
}
Usage
<!-- src/routes/+page.svelte -->
<script>
import { draggable } from '$lib/actions/draggable.js';
</script>
<div id="card1" use:draggable class="card">Drag me!</div>
<style>
.card {
padding: 1rem;
background: coral;
width: 120px;
border-radius: 8px;
cursor: grab;
}
.dragging {
opacity: 0.5;
}
</style>
✅ Perfect for building kanban boards, sortable lists, or any drag-n-drop UI.
Mini Recap 📝
So far you’ve learned:
- Actions attach reusable DOM behavior with the
use:
directive. -
They can:
- clean up (
destroy
) - react to parameter changes (
update
) - even dispatch custom events back to your components.
- clean up (
Together we built:
- autofocus (simple mount logic)
- clickOutside (cleanup on destroy)
- resize (parameters & updates)
- debounce (custom events)
- draggable (real DOM APIs)
Extra Notes for Beginners
🔹 Debounce:
When we call node.dispatchEvent(new CustomEvent(...))
, we’re basically saying:
“Pretend this is a normal DOM event.”
That’s why in your component you can listen with onDebounced
just like onClick
.
🔹 Draggable:
node.draggable = true;
is just enabling the browser’s built-in drag support.
We’re not moving elements around yet — just marking them as draggable and styling them while they’re being dragged. That’s why this version is “basic.” You could extend it later with drop zones or reordering logic.
✨ And that’s actions! Small functions that give your elements reusable superpowers.
Next: let’s explore children, Svelte’s secret weapon for flexible component composition.
Why Children? 👶
Props let parents send data to children.
Children let parents send content to children.
They’re the backbone of reusable UI libraries — think buttons, cards, layouts, modals. While actions manipulate the behavior of DOM elements (like adding click event listeners or resizing an element), children allow parents to inject content into components. Together, actions and children make your components more dynamic and reusable
👉 Children are like cutouts in your component where parents can inject their own content.
Default Children 📦
Sometimes you don’t just want your component to look nice — you want it to be a container for other stuff. That’s what the children
prop is for.
It’s like saying:
“Hey parent, you tell me what goes inside, and I’ll handle the wrapper.”
Example: Card Component
<!-- src/lib/components/Card.svelte -->
<script>
export let children;
</script>
<div class="card">
{@render children?.()}
</div>
<style>
.card {
border: 1px solid #ddd;
border-radius: 6px;
padding: 1rem;
}
</style>
Using the Card
<!-- src/routes/+page.svelte -->
<script>
import Card from '$lib/components/Card.svelte';
</script>
<Card>
<h2>Hello</h2>
<p>This is inside the card!</p>
</Card>
✨ Boom! Whatever you write between <Card> ... </Card>
gets passed into the children
prop and rendered inside.
Think of it like a bento box 🍱: the Card
provides the container, you provide the delicious filling.
Named Children 🧁
What if your component has multiple areas — like header, body, and footer — instead of just one?
You can accept named children, which are just props that are functions returning content.
Example: Layout Component
<!-- src/lib/components/Layout.svelte -->
<script>
export let header;
export let footer;
export let children; // default area
</script>
<header>{@render header?.()}</header>
<main>{@render children?.()}</main>
<footer>{@render footer?.()}</footer>
Using the Layout
<!-- src/routes/+page.svelte -->
<script>
import Layout from '$lib/components/Layout.svelte';
</script>
<Layout
header={() => <h1>My Site</h1>}
footer={() => <small>© 2025</small>}
>
<p>Main content goes here.</p>
</Layout>
✅ Each piece gets dropped exactly where you want it.
Think of it like a muffin tray 🧁: each “cup” has its own filling.
Fallback Children 📝
Sometimes parents don’t pass anything in. Should your component be an empty shell? Nope! You can provide default content instead.
Example: Button Component
<!-- src/lib/components/Button.svelte -->
<script>
export let children = () => "Click me";
</script>
<button>{@render children()}</button>
Using the Button
<!-- src/routes/+page.svelte -->
<script>
import Button from '$lib/components/Button.svelte';
</script>
<Button /> <!-- shows "Click me" -->
<Button>Save</Button> <!-- shows "Save" -->
✅ If the parent passes children, those take priority. Otherwise, the component politely falls back to its default.
It’s like leaving a friendly sticky note:
“Forgot to pass me something? No worries, I got you 👍”
Children as Render Functions (Child → Parent) 🔄
Here’s the really cool part: children don’t have to just be static content — they can be functions that receive data from the child component.
That means:
- The child decides when to render something (like looping through a list).
- The parent decides how it should look.
Example: List Component
<!-- src/lib/components/List.svelte -->
<script>
export let items = [];
export let children;
</script>
<ul>
{#each items as item}
<li>{@render children({ item })}</li>
{/each}
</ul>
Using the List
<!-- src/routes/+page.svelte -->
<script>
import List from '$lib/components/List.svelte';
</script>
<List items={['Ada', 'Grace', 'Linus']}>
{({ item }) => <span>{item.toUpperCase()}</span>}
</List>
✅ The List component controls when to render each <li>
.
✅ The parent controls what each item looks like — in this case, uppercased names.
It’s like the child saying: “Here’s the data,” and the parent replying: “Cool, I’ll dress it up.” 🎨
Gotchas & Best Practices ⚠️
- Always provide fallback children so your component doesn’t break if nothing is passed in.
- When looping with
{#each}
, always use keys (e.g.{#each items as item (item.id)}
) to avoid weird DOM recycling bugs. - Children-as-functions are powerful, but keep it simple — only pass the data the parent actually needs.
We covered a lot of ground here:
Actions: from a one-liner autofocus to advanced behaviors with cleanup, parameters, and custom events.
Children: from default content, to named sections, to render functions that let parents customize rendering logic.
These patterns make your components smarter and more reusable.
👉 Next, we’ll tie things together with Svelte Lifecycle Hooks and Accessibility — ensuring your components behave correctly over time and are usable by everyone.
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
Top comments (0)