Forward Props Flexibly
Sometimes you build wrapper components that need to pass props through to an inner element. Instead of listing every possible attribute, spread syntax forwards them all at once.
What You’ll Learn
- Spread props onto elements
- Combine explicit props with rest props
- Build wrapper components
- Handle HTML attributes in components
The Problem
You create a custom Button component:
<!-- filename: src/lib/components/Button.svelte -->
<script>
let { label, variant = 'primary' } = $props();
</script>
<button class="btn btn-{variant}">
{label}
</button> But users want to add standard HTML attributes:
<!-- User wants to add these -->
<Button
label="Submit"
type="submit" <!-- Not forwarded! -->
disabled={isLoading} <!-- Not forwarded! -->
onclick={handleClick} <!-- Not forwarded! -->
/> You’d have to add props for every HTML attribute. That’s tedious.
The Solution: Rest Props
Capture remaining props and spread them:
<!-- filename: src/lib/components/Button.svelte -->
<script>
let { label, variant = 'primary', ...rest } = $props();
</script>
<button class="btn btn-{variant}" {...rest}>
{label}
</button> The ...rest captures all props not explicitly destructured. Then {...rest} spreads them onto the element.
Now this works:
<Button
label="Submit"
type="submit"
disabled={isLoading}
onclick={handleClick}
/> All three attributes pass through to the <button> element.
How It Works
<script>
let { known, another, ...rest } = $props();
</script> If the component receives:
<Component known="a" another="b" extra="c" more="d" /> Then:
known= “a”another= “b”rest={ extra: "c", more: "d" }
Spreading {...rest} expands to extra="c" more="d" on the element.
Build a TextField Component
A common pattern — wrap an input with a label:
<!-- filename: src/lib/components/TextField.svelte -->
<script>
let {
label,
id,
error = '',
...rest
} = $props();
</script>
<div class="text-field" class:has-error={error}>
<label for={id}>{label}</label>
<input {id} {...rest} />
{#if error}
<p class="error">{error}</p>
{/if}
</div>
<style>
.text-field {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
}
input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.has-error input {
border-color: #dc3545;
}
.error {
color: #dc3545;
font-size: 0.875rem;
margin: 0.25rem 0 0 0;
}
</style> Usage:
<script>
import TextField from '$lib/components/TextField.svelte';
let email = $state('');
let emailError = $state('');
</script>
<TextField
label="Email Address"
id="email"
type="email"
bind:value={email}
placeholder="you@example.com"
required
error={emailError}
/> The type, bind:value, placeholder, and required all pass through to the input.
Combine with Class
What if both the component and caller want to add classes?
<script>
let { class: className = '', variant = 'primary', ...rest } = $props();
</script>
<button class="btn btn-{variant} {className}" {...rest}>
<slot />
</button> Usage:
<!-- Both classes apply -->
<Button class="full-width" variant="secondary">
Click Me
</Button>
<!-- Result: class="btn btn-secondary full-width" --> Note: class is a reserved keyword in JavaScript, so we rename it: class: className.
Wrapper Components
Spread props shine for wrapper components:
<!-- filename: src/lib/components/Card.svelte -->
<script>
let { padding = '1rem', ...rest } = $props();
</script>
<div class="card" style:padding {...rest}>
<!-- Content via snippet or children -->
</div>
<style>
.card {
border: 1px solid #ddd;
border-radius: 8px;
background: white;
}
</style> The Card accepts any <div> attributes:
<Card
id="main-card"
data-testid="service-card"
onclick={handleCardClick}
>
<!-- content -->
</Card> ServiceCard with Spread Props
Make ServiceCard more flexible:
<!-- filename: src/lib/components/ServiceCard.svelte -->
<script>
import PriceBadge from './PriceBadge.svelte';
let {
service,
selected = false,
...rest
} = $props();
</script>
<article class="service-card" class:selected {...rest}>
<h3>
<a href="/services/{service.slug}">{service.name}</a>
</h3>
<p>{service.description}</p>
<div class="meta">
<PriceBadge price={service.price} />
<span class="duration">{service.duration} min</span>
</div>
</article> Now callers can add any attributes:
<ServiceCard
{service}
selected={true}
data-category={service.category}
onclick={() => selectService(service)}
/> When Not to Spread
Spreading isn’t always appropriate:
Security-sensitive elements:
<!-- ❌ Don't blindly spread to links -->
<a {...rest}> <!-- Could inject href="javascript:..." -->
<!-- ✅ Validate or whitelist attributes -->
<a href={safeHref} {...rest}> Elements with specific requirements:
<!-- ❌ Spreading might override critical attributes -->
<form {...rest}> <!-- Could override method or action -->
<!-- ✅ Be explicit about critical attributes -->
<form method="POST" action="/submit" {...rest}> When types matter:
<!-- ❌ Props might not match element's expected types -->
<img {...rest} /> <!-- src and alt should be validated -->
<!-- ✅ Handle critical props explicitly -->
<script>
let { src, alt, ...rest } = $props();
</script>
<img {src} {alt} {...rest} /> Common Mistakes
Spreading Before Explicit Props
<!-- ❌ Explicit prop gets overwritten by spread -->
<button {...rest} class="btn">
<!-- ✅ Spread first, then explicit props -->
<button class="btn" {...rest}>
<!-- Or merge classes -->
<button {...rest} class="btn {rest.class || ''}"> Forgetting to Destructure Known Props
<script>
// ❌ 'variant' ends up in rest and on the element
let { ...rest } = $props();
</script>
<button {...rest}> <!-- data-variant="primary" on the element? -->
<script>
// ✅ Extract known props
let { variant, ...rest } = $props();
</script>
<button {...rest}> <!-- Clean --> Module Complete! 🎉
You’ve finished Module 7: Component Communication. BookIt now has:
- ServiceCard component
- PriceBadge component
- BookingForm component
- Props with defaults
- Spread props for flexibility
What you’ve learned:
- Why components improve maintainability
- How to create and import components
- Passing data with
$props() - Default values for optional props
- Spread props for flexible forwarding
Summary
Spread props (...rest) capture and forward unknown props to elements. This makes wrapper components flexible without listing every possible attribute. Use it for buttons, inputs, cards, and other generic wrappers.
Key takeaways:
let { known, ...rest } = $props()separates known from unknown props{...rest}spreads remaining props onto an element- Great for wrapper components
- Be careful with security-sensitive elements
Next Steps
Components look functional but plain. Continue with Module 8: Styling to make BookIt look professional.