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.