Let Customers Tell You More

Sometimes a dropdown isn’t enough. Customers might have specific instructions: “Enter through the side gate,” “Allergic to certain fertilizers,” or “Please call before arriving.” A notes textarea gives them space to share details.


What You’ll Learn

  • Bind textarea values
  • Add character limits
  • Show remaining characters
  • Make textareas auto-resize

Basic Textarea Binding

Textareas bind just like inputs:

<script>
  let notes = $state('');
</script>

<label for="notes">Additional Notes (Optional)</label>
<textarea 
  id="notes"
  bind:value={notes}
  placeholder="Any special instructions or requests..."
></textarea>

<p>Notes: {notes || 'None provided'}</p>

The bind:value keeps the textarea content synchronized with state.


Add a Character Limit

Prevent excessively long notes:

<script>
  let notes = $state('');
  const maxLength = 500;
  
  let remaining = $derived(maxLength - notes.length);
  let isOverLimit = $derived(notes.length > maxLength);
</script>

<label for="notes">Additional Notes (Optional)</label>
<textarea 
  id="notes"
  bind:value={notes}
  placeholder="Any special instructions or requests..."
  maxlength={maxLength}
></textarea>

<p class="char-count" class:warning={remaining < 50} class:error={isOverLimit}>
  {remaining} characters remaining
</p>

<style>
  .char-count {
    font-size: 0.875rem;
    color: #666;
    margin-top: 0.25rem;
  }
  
  .char-count.warning {
    color: #f59e0b;
  }
  
  .char-count.error {
    color: #ef4444;
  }
</style>

The maxlength attribute prevents typing beyond the limit. The character counter provides feedback.


Enforce Limit Without Attribute

If you want custom handling instead of the native maxlength:

<script>
  let notes = $state('');
  const maxLength = 500;
  
  function handleInput(event) {
    const value = event.target.value;
    if (value.length <= maxLength) {
      notes = value;
    } else {
      // Trim to max length
      notes = value.slice(0, maxLength);
      // Force the textarea to show trimmed value
      event.target.value = notes;
    }
  }
</script>

<textarea 
  value={notes}
  oninput={handleInput}
  placeholder="Any special instructions..."
></textarea>

This approach lets you add custom behavior when the limit is reached.


Auto-Resizing Textarea

Make the textarea grow as the user types:

<script>
  let notes = $state('');
  let textarea;
  
  function autoResize() {
    if (textarea) {
      textarea.style.height = 'auto';
      textarea.style.height = textarea.scrollHeight + 'px';
    }
  }
  
  $effect(() => {
    // Resize whenever notes change
    notes;
    autoResize();
  });
</script>

<textarea 
  bind:this={textarea}
  bind:value={notes}
  oninput={autoResize}
  placeholder="Any special instructions..."
  rows="3"
></textarea>

<style>
  textarea {
    resize: none;
    overflow: hidden;
    min-height: 80px;
  }
</style>

bind:this gives us a reference to the DOM element, allowing us to manipulate its height.


Add to the Booking Form

Here’s the complete notes section:

<!-- filename: src/lib/components/BookingForm.svelte (partial) -->
<script>
  let notes = $state('');
  const maxLength = 500;
  
  let remaining = $derived(maxLength - notes.length);
</script>

<fieldset>
  <legend>Additional Information</legend>
  
  <div class="field">
    <label for="notes">
      Special Instructions (Optional)
    </label>
    <textarea 
      id="notes"
      name="notes"
      bind:value={notes}
      placeholder="Examples:
• Gate code: 1234
• Please text before arriving
• Dog is friendly but loud
• Specific areas to focus on"
      maxlength={maxLength}
      rows="4"
    ></textarea>
    <p class="char-count" class:warning={remaining < 50}>
      {remaining} characters remaining
    </p>
  </div>
</fieldset>

<style>
  textarea {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 1rem;
    font-family: inherit;
    line-height: 1.5;
    resize: vertical;
    min-height: 100px;
  }
  
  textarea:focus {
    outline: none;
    border-color: #007bff;
    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
  }
  
  textarea::placeholder {
    color: #999;
  }
  
  .char-count {
    font-size: 0.875rem;
    color: #666;
    margin: 0.25rem 0 0 0;
    text-align: right;
  }
  
  .char-count.warning {
    color: #f59e0b;
  }
</style>

The Complete Booking Form

With all fields in place, here’s the full form structure:

<!-- filename: src/lib/components/BookingForm.svelte -->
<script>
  // Contact information
  let customerName = $state('');
  let customerEmail = $state('');
  let customerPhone = $state('');
  
  // Appointment details
  let bookingDate = $state('');
  let selectedTime = $state('');
  
  // Service selection
  let selectedService = $state('');
  let selectedAddOns = $state([]);
  
  // Additional info
  let notes = $state('');
  let agreeToTerms = $state(false);
  
  // Form state
  let submitted = $state(false);
  
  function handleSubmit(event) {
    event.preventDefault();
    submitted = true;
    // In a later module, we'll send this to the server
  }
  
  function resetForm() {
    customerName = '';
    customerEmail = '';
    customerPhone = '';
    bookingDate = '';
    selectedTime = '';
    selectedService = '';
    selectedAddOns = [];
    notes = '';
    agreeToTerms = false;
    submitted = false;
  }
</script>

{#if submitted}
  <div class="confirmation">
    <h2>✓ Booking Request Received!</h2>
    <p>Thank you, {customerName}! We'll confirm your booking shortly.</p>
    <button onclick={resetForm}>Book Another Service</button>
  </div>
{:else}
  <form onsubmit={handleSubmit}>
    <fieldset>
      <legend>Contact Information</legend>
      <!-- Name, email, phone fields -->
    </fieldset>
    
    <fieldset>
      <legend>Appointment Details</legend>
      <!-- Date and time fields -->
    </fieldset>
    
    <fieldset>
      <legend>Select a Service</legend>
      <!-- Service radio buttons -->
    </fieldset>
    
    <fieldset>
      <legend>Add-Ons (Optional)</legend>
      <!-- Add-on checkboxes -->
    </fieldset>
    
    <fieldset>
      <legend>Additional Information</legend>
      <!-- Notes textarea -->
    </fieldset>
    
    <label class="terms">
      <input type="checkbox" bind:checked={agreeToTerms} required />
      I agree to the terms and conditions
    </label>
    
    <button type="submit" disabled={!agreeToTerms}>
      Request Booking
    </button>
  </form>
{/if}

Common Mistakes

Using value Attribute Instead of Binding

<!-- ❌ Content won't update with state -->
<textarea value={notes}></textarea>

<!-- ✅ Use bind:value for two-way sync -->
<textarea bind:value={notes}></textarea>

Forgetting font-family

/* ❌ Textarea has different default font */
textarea {
  font-size: 1rem;
}

/* ✅ Match the rest of your form */
textarea {
  font-size: 1rem;
  font-family: inherit;
}

Not Setting min-height

/* ❌ Textarea can be squished too small */
textarea {
  resize: vertical;
}

/* ✅ Ensure minimum usable height */
textarea {
  resize: vertical;
  min-height: 100px;
}

Module Complete! 🎉

You’ve finished Module 6: Data Binding. The booking form now has:

  • Text inputs for name and email
  • Date picker for appointment date
  • Time slot dropdown
  • Service selection with radio buttons
  • Add-on checkboxes
  • Notes textarea
  • Terms checkbox

What you’ve learned:

  • bind:value for text, email, date inputs
  • bind:value for select dropdowns
  • bind:group for radio buttons and checkbox groups
  • bind:checked for single checkboxes
  • Character limits and counters

Summary

Textareas bind with bind:value just like other inputs. Add character limits for better UX, and consider auto-resize for longer content. The booking form is now feature-complete on the client side.

Key takeaways:

  • bind:value works with textarea
  • Use maxlength and character counters
  • Set font-family: inherit for consistent styling
  • Auto-resize improves UX for longer content

Next Steps

The form works, but it’s all in one file. Continue with Module 7: Component Communication to break it into reusable components.