Quick question: why do modern apps feel so smooth, even though they’re insanely complicated under the hood?
Open Amazon and peek at a product page. You’ll spot:
a shiny product gallery,
a reviews section full of opinions,
“you might also like” recommendations,
a payment form ready to take your money,
and of course, the login box that never remembers your password 🙃.
All of that looks like one seamless page, but behind the scenes each piece is its own little universe — often built by different teams, at different times, maybe even in different programming languages. Yet, somehow, it all just… works.
Same deal on Facebook: the news feed, stories, chat window, notifications — they’re all separate chunks, but together they create the illusion of one big, flowing app.
That’s the magic of components. Components let us break big, messy apps into smaller, reusable building blocks — like Lego. One block = one responsibility. Snap them together, and voilà: a complete app.
And Svelte makes this extra delightful, because a component is just a .svelte file. That’s it. No boilerplate. No ceremony. Just a file you import and drop into your app like it was born to be there.
But here’s where it gets even cooler 🤩: Svelte hands you a few little superpowers for handling data inside components:
$props() → mark the inputs your component gets from its parent,
$state() → keep track of local data that belongs to your component,
$derived() → compute new values automatically when other data changes.
Think of these as your toolkit for making components talk to each other, stay in sync, and do useful things without you manually wiring everything together.
In this guide, we’ll explore these helpers one by one, with tons of examples. By the end, you’ll be thinking in components like a pro — and maybe even having fun doing it. 🎉
Quick Setup Reminder 🛠️
As before, we’ll follow the usual SvelteKit project layout:
Put reusable components inside the src/lib folder.
Use those components inside your main page at src/routes/+page.svelte. (Yes it would be wiped cleean/replaced for each example)
Make sure you issued the npm run dev command in terminal for your project.
(If you landed here directly: this is the standard setup in SvelteKit. Think of lib as your toolbox of building blocks, and routes as the actual rooms in your house where you use those blocks.)
Section 1: Components Working Together
Now, let’s start simple. A single component is nice, but the real magic happens when you snap them together like Lego bricks.
Imagine a page with three parts: a header, a product list, and a footer. Each lives in its own file.
👉 Quick note: A .svelte file (a component) can optionally contain three sections:
a block for logic,
plain HTML markup for what it renders,
a block for CSS that applies only to that component.
For our first example, we don’t need any logic or special styling, so each file is just markup. (And is a fully functional svelte component)
The Header component
Welcome to My Store
This is our “welcome mat.” Its only job is to display a title at the top of the page.
The ProductList component
Apples
Bananas
Cherries
This is the “main attraction.” For now, it’s just a static list of products (fruit). Later we’ll make it dynamic.
tells Svelte: “This component expects the parent to provide a value called name.” You can think of $props() as a basket that holds all the values the parent hands down. By writing { name }, we’re pulling the name value out of that basket so we can use it.
In +page.svelte, we pass values down like this:
name="Ada"/>name="Alan"/>
That’s just like giving a src attribute — except now the attribute is name, and it belongs to our custom component.
When the page renders, the child component replaces {name} in its markup with the actual values from the parent. So you’ll see:
Hello, Ada!
Hello, Alan!
👉 Mental model:
$props() = “gives me a basket of all the values my parent passes in.”
let { name } = $props(); = “take the name value out of that basket so I can use it in this file.”
Props in general = “like attributes in HTML, e.g. , but for your own components.”
Default Values for Props
What if a parent forgets to provide a prop? No problem — you can give props default values when destructuring:
let{user}=$props();{user.name} is {user.age} years old.
Shortcut alert 🚨: if the prop name and the variable name match, you can write:
{user} />
This is equivalent to user={user} — just shorter and cleaner.
✅ Now props are no longer mysterious: they’re just the way parents hand down values to children, exactly like attributes in HTML.
Section 3: State vs Props
At this point, you might be wondering:
“Okay, props are values a parent hands down. But what about data that belongs to the component itself?”
Great question. This is the difference between props and state:
Props → data a parent gives to a child. They’re external inputs.
State → data the component owns and manages by itself. It can change inside the component, often when the user clicks, types, or interacts with something.
Think of it like this:
Props are like the ingredients someone else hands you.
State is the mixing bowl you keep in your own kitchen — you can stir it, add to it, or reset it whenever you like.
Example: A Counter Component
You’ve met this one before back in our reactivity article — the trusty counter. Let’s see it again in its simplest form:
src/lib/Counter.svelte
// Start with count = 0, owned by this componentletcount=$state(0);
Clicking the button increases count, and the UI updates automatically. That’s state in action.
Mixing Props and State Together
Now let’s combine what we know. What if we want a counter that starts at a value given by the parent, but can then be updated inside the component?
Props give us the starting point. State takes over from there.
src/lib/StartCounter.svelte
// Expect an "initial" value from the parentlet{initial}=$props();// Use that initial value as the starting stateletcount=$state(initial);
The parent page provides an initial value for each counter.
Inside StartCounter.svelte, we grab that prop with $props() and use it to set up local state.
From then on, the component owns count. Clicking the button updates its own state without affecting the parent.
⚠️ Gotcha: Don’t Mutate Props Directly
If you try to do this:
let{initial}=$props();initial++;// ❌ changing a prop directly
…it won’t work the way you expect. Props are read-only inputs. They belong to the parent.
If you need to change a value, copy it into state first, like we did with count = $state(initial). That way the child owns its own copy and can update it freely.
👉 Rule of thumb:
Use $props() for values that come from the outside.
Use $state() for values you control inside.
Section 4: Composition — Building Reusable Blocks
Now that you know how to pass data down with $props() and manage local data with $state(), let’s talk about composition.
Composition is just a fancy word for putting components together. You’ve already seen it at a big scale when we combined Header, ProductList, and Footer into a full page. That’s like arranging the rooms of a house.
But composition also happens at smaller scales: little reusable widgets (like buttons) or nested components (like a card with its own header and body). Think of these as the doorknobs and light switches inside those rooms.
Example 1: A Reusable Button
Let’s build a button once and use it everywhere.
src/lib/Button.svelte
// Accept a "label" prop, with a default fallbacklet{label="Click me"}=$props();{label}
src/routes/+page.svelte
importButtonfrom'$lib/Button.svelte';
My App
Here, we wrote the button once, but we can drop it into the page as many times as we want with different labels. If we later style or improve the button, every copy updates automatically.
That’s the “reusable building block” side of composition.
Example 2: Nested Components (a Card)
Now let’s see the nesting side of composition. Imagine you want a card layout, like you see on product pages. You can break it down into smaller components — CardHeader and CardBody — and then compose them together in a Card.
importCardfrom'$lib/Card.svelte';title="Banana"content="A yellow fruit that's great in smoothies."/>title="Cherry"content="Small, red, and perfect on top of desserts."/>
Here’s the breakdown:
CardHeader and CardBody are small, focused components.
Card.svelte nests them together to form a bigger reusable piece.
In +page.svelte, we drop in multiple Card components with different props.
That’s the nesting side of composition: small parts joining forces to make bigger parts.
Why This Matters
Composition is what makes large apps manageable:
Reusability: Write once, use everywhere (like the Button).
Nesting: Build small, combine into bigger, repeat (like the Card).
It’s Lego all the way down. 🧩
Section 5: Derived Values with $derived()
By now you know how to pass data down with props and manage local state. But sometimes you want a new value that’s calculated from them, and you want that calculation to always stay fresh if the inputs change.
That’s exactly what $derived() is for.
A Quick Note on How Props Update
When a parent changes a prop, the child doesn’t get torn down and rebuilt from scratch. Svelte is smarter than that.
Instead, Svelte updates just the prop variable inside the child.
That means:
If you wrote let { discount } = $props();, then discount will update when the parent changes it.
But if you wrote let discountedPrice = price - (price * discount);, that calculation only ran once when the component was created. It won’t re-run automatically.
This is why $derived() exists — it tells Svelte: “keep this calculation in sync whenever its inputs change.”
Example: Discounted Price
Let’s make this concrete with a product card that shows both the original price and the discounted price.
src/lib/ProductCard.svelte
let{price,discount}=$props();// Derived value: automatically update when price or discount changesletdiscountedPrice=$derived(price-(price*discount));
Original price: ${price}
Discounted price: ${discountedPrice}
src/routes/+page.svelte
importProductCardfrom'$lib/ProductCard.svelte';letprice=100;letdiscount=$state(0.1);// 10% off to startfunctionapplyCoupon(){discount=0.25;// bump discount to 25%}{price}{discount} />
on:click={applyCoupon}>Apply Coupon
Here’s the flow:
At first, the UI shows:
Original price: $100
Discounted price: $90
You click the button in the parent. It updates discount to 0.25.
Because discountedPrice is declared with $derived(), it recalculates automatically in the child.
The child’s UI updates to:
Original price: $100
Discounted price: $75
If we had written:
let discountedPrice = price - (price * discount); // ❌ plain let
…the discounted price would stay stuck at $90, even after the parent changed the discount to 25%.
That’s the tangible benefit of $derived(): it makes sure your computed values stay reactive, not frozen in time.
👉 Rule refresher:
$props() → external input from the parent.
$state() → internal data you control.
$derived() → computed values that automatically update when their sources change.
Section 6: Gotchas with Props ⚠️
Before you run off and build an empire of components, let’s talk about a few easy-to-make mistakes that trip up beginners all the time. Knowing these ahead of time will save you from hours of head-scratching.
1. Forgetting $props()
If you just write:
letname;// ❌ not a prop
…it won’t work as a prop. The parent can pass name="Ada" all day long, but the child won’t receive it.
You must grab props with $props():
let{name}=$props();// ✅ now it's a prop
Think of $props() as the “basket” that holds all the values the parent passes down. If you don’t reach into that basket, your component stays empty-handed.
2. Reserved Words
JavaScript already has some words with special meaning: class, for, let, etc. You can’t use those as prop names.
Bad idea:
class="highlighted"/>
Good idea:
cssClass="highlighted"/>
When in doubt, just pick a slightly different name.
3. One-Way Flow
Props always flow downward. Parents talk, children listen. If the parent changes a prop, the child updates. But children can’t directly change the parent’s prop.
✅ If you click the button in the parent, the child sees the updated value. ❌ The child itself cannot say value++ to update the parent’s count.
Props are inputs, not two-way connections. (Don’t worry — children can talk back using events and bindings. That’s our next topic.)
Section 7: Wrap-Up 🎯
Let’s recap everything we’ve built up so far:
Components are the Lego blocks of apps.
$props() lets parents pass data down to children.
Props can have defaults, dynamic values, or even whole objects.
$state() manages data that a component owns.
$derived() computes new values that stay synced with props or state.
Composition works at the large scale (headers, footers) and the small scale (buttons, inputs, cards).
Props flow one way: parent → child.
Reactivity is triggered by assignments, not deep mutations.
That’s a rock-solid foundation already! 🎉
Outro: What’s Next 🚀
So far, our components are polite little citizens: parents hand down props, children quietly accept them.
But apps aren’t one-way streets. A child button might need to tell its parent “Hey, I was clicked!” A form might need to notify its parent when it’s submitted.
👉 That’s where events and bindings come in — the backbone of two-way communication in Svelte. We’ll dive into that in the next article.