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
<script>
block for logic, - plain HTML markup for what it renders,
- a
<style>
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
<!-- src/lib/Header.svelte -->
<header>
<h1>Welcome to My Store</h1>
</header>
This is our “welcome mat.” Its only job is to display a title at the top of the page.
The ProductList component
<!-- src/lib/ProductList.svelte -->
<ul>
<li>Apples</li>
<li>Bananas</li>
<li>Cherries</li>
</ul>
This is the “main attraction.” For now, it’s just a static list of products (fruit). Later we’ll make it dynamic.
The Footer component
<!-- src/lib/Footer.svelte -->
<footer>
<p>© 2025 My Store</p>
</footer>
This is our “goodbye.” It sits at the bottom and shows copyright text.
Putting it all together
Finally, let’s combine them inside our main page:
<!-- src/routes/+page.svelte -->
<script>
import Header from '$lib/Header.svelte';
import ProductList from '$lib/ProductList.svelte';
import Footer from '$lib/Footer.svelte';
</script>
<Header />
<ProductList />
<Footer />
Here’s what’s happening:
- The
<script>
block uses import statements to pull each component into this file. -
$lib
is SvelteKit’s shortcut forsrc/lib
, so'$lib/Header.svelte'
means “grab the file atsrc/lib/Header.svelte
.” - In the markup,
<Header />
,<ProductList />
, and<Footer />
render those components right where you place them — just like custom HTML tags.
Boom 💥 — now your page is modular. Each component minds its own business (markup, style, behavior), but together they create one smooth experience.
This sets the stage for our next question: how do these components actually talk to each other? That’s where props come in.
Section 2: Props — Passing Data Down
Placing components side by side is nice, but usually, a parent component needs to pass data to a child.
Think about it:
- A greeting component needs to know who it’s greeting (
name
). - A button needs to know what text it should show (
label
). - An image needs to know which picture to display (
src
).
That information has to come from somewhere. In Svelte, that “somewhere” is props.
Props are like inputs to a component. They’re the way a parent gives values to its child. If you’ve ever written HTML like this:
<img src="/cat.png" alt="A cat" />
…the src
and alt
attributes are basically props for the built-in <img>
element. Components you write work the same way.
Inside a .svelte
file, you mark which values your component expects by pulling them out of $props()
.
Hello World with Props
Let’s build the simplest example:
src/lib/Child.svelte
<script>
// Expect a "name" value from the parent
let { name } = $props();
</script>
<p>Hello, {name}!</p>
src/routes/+page.svelte
<script>
import Child from '$lib/Child.svelte';
</script>
<Child name="Ada" />
<Child name="Alan" />
Here’s what’s happening:
- In
Child.svelte
, the line
let { name } = $props();
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:
<Child name="Ada" />
<Child name="Alan" />
That’s just like giving <img>
a src
attribute — except now the attribute is name
, and it belongs to our custom <Child>
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.
<img src="cat.png">
, 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:
src/lib/Child.svelte
<script>
let { greeting = "Hello", name = "stranger" } = $props();
</script>
<p>{greeting}, {name}!</p>
Now if you render <Child />
in your page without any props, it shows:
Hello, stranger!
Defaults are lifesavers for optional values like placeholders, styles, or fallback text.
Passing Dynamic Values
Props don’t have to be static strings. You can pass any JavaScript value — numbers, objects, arrays, even functions.
src/routes/+page.svelte
<script>
import Child from '$lib/Child.svelte';
let user = { name: "Grace", age: 36 };
</script>
<Child user={user} />
Inside src/lib/Child.svelte
:
<script>
let { user } = $props();
</script>
<p>{user.name} is {user.age} years old.</p>
Shortcut alert 🚨: if the prop name and the variable name match, you can write:
<Child {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
<script>
// Start with count = 0, owned by this component
let count = $state(0);
</script>
<button on:click={() => count++}>
Count: {count}
</button>
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
<script>
// Expect an "initial" value from the parent
let { initial } = $props();
// Use that initial value as the starting state
let count = $state(initial);
</script>
<button on:click={() => count++}>
Count: {count}
</button>
src/routes/+page.svelte
<script>
import StartCounter from '$lib/StartCounter.svelte';
</script>
<!-- Parent passes different initial values -->
<StartCounter initial={0} />
<StartCounter initial={10} />
Here’s what happens:
- 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:
<script>
let { initial } = $props();
initial++; // ❌ changing a prop directly
</script>
…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
<script>
// Accept a "label" prop, with a default fallback
let { label = "Click me" } = $props();
</script>
<button>{label}</button>
src/routes/+page.svelte
<script>
import Button from '$lib/Button.svelte';
</script>
<h1>My App</h1>
<!-- Same component, different labels -->
<Button label="Save" />
<Button label="Cancel" />
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
.
src/lib/CardHeader.svelte
<script>
let { title } = $props();
</script>
<div class="card-header">
<h2>{title}</h2>
</div>
src/lib/CardBody.svelte
<script>
let { content } = $props();
</script>
<div class="card-body">
<p>{content}</p>
</div>
src/lib/Card.svelte
<script>
import CardHeader from '$lib/CardHeader.svelte';
import CardBody from '$lib/CardBody.svelte';
let { title, content } = $props();
</script>
<div class="card">
<CardHeader {title} />
<CardBody {content} />
</div>
Finally, use it in the page:
src/routes/+page.svelte
<script>
import Card from '$lib/Card.svelte';
</script>
<Card title="Banana" content="A yellow fruit that's great in smoothies." />
<Card title="Cherry" content="Small, red, and perfect on top of desserts." />
Here’s the breakdown:
-
CardHeader
andCardBody
are small, focused components. -
Card.svelte
nests them together to form a bigger reusable piece. - In
+page.svelte
, we drop in multipleCard
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();
, thendiscount
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
<script>
let { price, discount } = $props();
// Derived value: automatically update when price or discount changes
let discountedPrice = $derived(price - (price * discount));
</script>
<p>Original price: ${price}</p>
<p>Discounted price: ${discountedPrice}</p>
src/routes/+page.svelte
<script>
import ProductCard from '$lib/ProductCard.svelte';
let price = 100;
let discount = $state(0.1); // 10% off to start
function applyCoupon() {
discount = 0.25; // bump discount to 25%
}
</script>
<ProductCard {price} {discount} />
<button on:click={applyCoupon}>Apply Coupon</button>
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
to0.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:
<script>
let name; // ❌ not a prop
</script>
…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()
:
<script>
let { name } = $props(); // ✅ now it's a prop
</script>
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:
<Child class="highlighted" /> <!-- ❌ won't work as a prop -->
Good idea:
<Child cssClass="highlighted" /> <!-- ✅ works fine -->
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.
Example:
src/routes/+page.svelte
<script>
import Child from '$lib/Child.svelte';
let count = $state(0);
</script>
<Child value={count} />
<button on:click={() => count++}>Increment</button>
src/lib/Child.svelte
<script>
let { value } = $props();
</script>
<p>Child sees: {value}</p>
✅ 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.
Next: Svelte Events & Bindings Tutorial: Master Parent-Child Communication
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)