The Heart of BookIt
Every booking app needs a form — it’s the primary way customers interact with your service. They enter their details, select what they want, choose when they want it, and submit their booking. Without this form, BookIt would just be a static brochure.
In this lesson, you’ll build this form as a reusable component. Components are one of Svelte’s most powerful features. Instead of copy-pasting the same form HTML across multiple pages, you’ll write it once and use it anywhere.
What is a Component?
Think of a component as a custom HTML tag that you define. Just like you can write <button> or <input> and they work anywhere, you can create a <BookingForm> tag and use it wherever you need a booking form.
Components encapsulate:
- Structure (HTML markup)
- Behavior (JavaScript logic)
- Styling (CSS)
This makes your codebase modular, maintainable, and easier to reason about.
What You’ll Learn
- Create a new component file
- Build basic form structure with HTML
- Import and use the component in a page
Create the Component File
Components in Svelte are simply .svelte files — that’s it. Any file with a .svelte extension is a component that can be imported and reused throughout your application.
The src/lib Folder Convention
SvelteKit has a special folder called src/lib for shared code. This is where you put:
- Reusable components (like our booking form)
- Utility functions (date formatters, validators, etc.)
- Type definitions (TypeScript interfaces)
- Constants and configuration
Anything you put in src/lib can be imported from anywhere in your app using the $lib alias (more on this soon).
Creating the Component
Lets create our first Svelte component:
- Create the
src/lib/components/directory if it doesn’t exist - Inside it, create a new file called
BookingForm.svelte
src/
├── lib/ ← Shared code
│ └── components/ ← Reusable UI components
│ └── BookingForm.svelte ← Create this file
└── routes/ ← Pages and endpoints
└── ... Why components/? While you could put BookingForm.svelte directly in src/lib, creating a components/ subfolder keeps things organized. As your app grows, you might have dozens of components, and having them in their own folder prevents clutter.
Naming convention: Component files are typically named in PascalCase (like BookingForm.svelte), matching the way you’ll use them: <BookingForm />. This makes it clear at a glance that it’s a component, not a utility function or constant.
Build the Form Structure
Now let’s build the actual form. We’ll start with plain, semantic HTML — no JavaScript, no reactivity, just a well-structured form. Getting the HTML foundation right is crucial because it affects accessibility, user experience, and how easy it will be to add interactivity later.
What Makes a Good Form?
Before we write code, let’s think about what a booking form needs:
- Clear labels — Users should know what each field is for
- Appropriate input types —
type="email"for email,type="date"for dates, etc. - Helpful placeholders — Examples of what to enter
- Logical grouping — Related fields grouped together
- Accessible markup — Labels associated with inputs for screen readers
Building the HTML
Add this HTML structure to your BookingForm.svelte file:
<!-- filename: src/lib/components/BookingForm.svelte -->
<form>
<h2>Book a Service</h2>
<div class="field">
<label for="name">Your Name</label>
<input type="text" id="name" name="name" placeholder="Jane Smith" />
</div>
<div class="field">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" placeholder="jane@example.com" />
</div>
<div class="field">
<label for="date">Preferred Date</label>
<input type="date" id="date" name="date" />
</div>
<div class="field">
<label for="time">Preferred Time</label>
<select id="time" name="time">
<option value="">Select a time...</option>
<option value="09:00">9:00 AM</option>
<option value="10:00">10:00 AM</option>
<option value="11:00">11:00 AM</option>
<option value="13:00">1:00 PM</option>
<option value="14:00">2:00 PM</option>
<option value="15:00">3:00 PM</option>
</select>
</div>
<div class="field">
<label for="notes">Additional Notes</label>
<textarea id="notes" name="notes" rows="3" placeholder="Any special requests?"></textarea>
</div>
<button type="submit">Request Booking</button>
</form> Understanding the Structure
Let’s break down what each part does:
The form wrapper:
<form> The <form> element wraps all inputs. When users press Enter or click the submit button, the form triggers a submit event. We’ll handle that in later lessons.
Field wrapper pattern:
<div class="field">
<label for="name">Your Name</label>
<input type="text" id="name" name="name" placeholder="Jane Smith" />
</div> Each field is wrapped in a <div class="field">. This:
- Groups the label and input together
- Makes styling easier (we can target
.fieldin CSS) - Creates semantic sections in the form
The for and id connection:
- The label’s
for="name"attribute matches the input’sid="name" - This associates the label with the input
- Clicking the label focuses the input
- Screen readers announce the label when the input is focused
Input types matter:
type="text"— Plain text input for namestype="email"— Validates email format, shows email keyboard on mobiletype="date"— Shows a date picker in modern browsers<select>— Dropdown for choosing from preset options<textarea>— Multi-line text for notes
Why this is plain HTML: Notice there’s no $state, no reactive variables, no event handlers. This is intentional. We’re building the structure first, then adding behavior. This is good practice — get the HTML right, then make it interactive.
Use the Component
Now that we’ve created the form component, let’s use it on the service detail page. This is where users will see service information and be able to book that specific service.
How Component Imports Work
In Svelte, using a component is a two-step process:
- Import the component in your
<script>tag - Use it in your markup like an HTML tag
The component name becomes a custom HTML tag. If you import BookingForm, you use it as <BookingForm />. The self-closing slash (/>) is important — components without children should be self-closing.
Adding to the Service Page
Update src/routes/services/[slug]/+page.svelte:
<!-- filename: src/routes/services/[slug]/+page.svelte -->
<script>
import BookingForm from '$lib/components/BookingForm.svelte'
let { data } = $props()
</script>
<nav>
<a href="/services">← Back to Services</a>
</nav>
{#if data.service}
<h1>{data.service.name}</h1>
<p>{data.service.description}</p>
<dl>
<dt>Price</dt>
<dd>${data.service.price}</dd>
<dt>Duration</dt>
<dd>{data.service.duration} minutes</dd>
</dl>
<BookingForm />
{:else}
<h1>Service Not Found</h1>
<p>We couldn't find that service.</p>
<a href="/services">Browse all services</a>
{/if} What Changed?
Two small but crucial additions:
1. The import statement:
import BookingForm from '$lib/components/BookingForm.svelte'; This tells Svelte: “I want to use the BookingForm component from the file at src/lib/components/BookingForm.svelte.”
import BookingForm— The name you’ll use in your markup (can be anything, but should match the filename)from '$lib/components/BookingForm.svelte'— Where to find the component- The
.svelteextension is required (unlike some frameworks where you can omit it)
2. Using the component:
<BookingForm /> This renders the component at this location in your page. It’s like placing the entire contents of BookingForm.svelte right here, but in a clean, reusable way.
The / at the end means this is a self-closing tag (no children). For components with children, you’d write <BookingForm>content</BookingForm>, but our form doesn’t need that.
Where in the Page?
We placed <BookingForm /> after the service details:
<h1>{data.service.name}</h1>
<p>{data.service.description}</p>
<dl>...</dl>
<BookingForm /> ← Added here This creates a logical flow: users read about the service, then immediately see how to book it.
The $lib Alias
Notice we imported from $lib/components/BookingForm.svelte, not a relative path like ../../../lib/components/BookingForm.svelte.
SvelteKit provides the $lib alias that always points to src/lib. This makes imports cleaner and avoids the ../../../ mess as your project grows.
<!-- ❌ Fragile — breaks if you move the file -->
import BookingForm from '../../../lib/components/BookingForm.svelte';
<!-- ✅ Stable — always works from anywhere -->
import BookingForm from '$lib/components/BookingForm.svelte'; Test the Form
Let’s verify that everything works. Make sure your dev server is running (npm run dev or pnpm dev) and visit a service detail page.
Testing Steps
Navigate to a service:
- Go to
http://localhost:5173/services - Click any service (e.g., “Lawn Mowing”)
- Or directly visit
http://localhost:5173/services/lawn-mowing
- Go to
Check the layout: You should see (from top to bottom):
- Service name as a heading
- Service description
- Price and duration information
- The booking form with all fields
Try interacting with the form:
- Type in the “Your Name” field — text appears
- Type in the “Email Address” field — text appears
- Click the date picker — calendar opens
- Select a time from the dropdown — option changes
- Type in the notes field — text appears
Click the submit button:
- The page refreshes
- All your typed data disappears
- Nothing is saved or sent anywhere
What’s Working and What’s Not
✅ Working:
- The form displays correctly
- All fields are visible and styled
- You can type and select values
- The HTML structure is sound
❌ Not working yet:
- Typed values aren’t tracked in JavaScript
- Nothing happens on form submission
- No validation
- No preview of the booking
- Data isn’t saved anywhere
Why It Feels “Broken”
The form might feel broken because it doesn’t do anything yet. This is expected! We’ve built the structure (the HTML skeleton), but we haven’t added the behavior (the JavaScript that tracks and responds to user input).
That’s what we’ll fix in the next lesson by adding $state to track form fields.
Why Components?
You might be wondering: “Why create a separate file? Why not just put the form HTML directly in the service page?”
This is a great question, especially if you’re new to component-based development. Let’s explore the benefits.
1. Reusability: Write Once, Use Everywhere
Imagine you decide to also show the booking form on:
- A dedicated
/bookpage - A modal popup when users click “Quick Book”
- An admin page for creating test bookings
Without a component, you’d copy-paste the form HTML to all these places. Now you have four copies of the same form. If you need to add a new field, you change it four times. If you find a bug, you fix it four times. This is a maintenance nightmare.
With a component:
<!-- Any page -->
<script>
import BookingForm from '$lib/components/BookingForm.svelte'
</script>
<BookingForm /> One component, infinite uses. Change it once, updates everywhere.
2. Organization: Separation of Concerns
Pages should focus on page-level concerns:
- What data to load
- Page layout and structure
- Which components to show
They shouldn’t be cluttered with the details of every form, modal, or complex widget. Components let you separate what (the page) from how (the component implementation).
Compare:
<!-- ❌ Page does everything — hard to scan -->
<h1>Service Details</h1>
<!-- 150 lines of form HTML -->
<!-- 50 lines of form logic -->
<!-- 80 lines of form styling --> Versus:
<!-- ✅ Page delegates to component — easy to understand -->
<h1>Service Details</h1>
<BookingForm /> The second version is easier to understand at a glance.
3. Isolation: Prevent Bugs
When form logic lives in the same file as page logic, they can interfere with each other. Variable names might conflict. Event handlers might accidentally affect the wrong elements.
Components create boundaries. The form’s logic stays in BookingForm.svelte. The page’s logic stays in +page.svelte. They communicate through well-defined interfaces (props and events), not by sharing global state.
4. Testability
Need to test the booking form? If it’s a component, you can:
<script>
import BookingForm from '$lib/components/BookingForm.svelte'
import { mount } from 'svelte'
// Test the form in isolation
const form = mount(BookingForm, { target: document.body })
</script> If it’s embedded in a page, you’d have to test the entire page, service data loading, routing, and everything else just to test the form.
Real-World Parallel
Think about cooking. You could write a recipe as one giant paragraph describing everything from shopping to plating. Or you could break it into sections: ingredients, preparation, cooking, serving.
Components are like those sections — they break complex UIs into manageable, named pieces.
When to Create Components
As a rule of thumb, create a component when:
- You’ll use it in multiple places
- It’s complex enough to clutter the parent file
- It represents a distinct UI concept (like a form, card, modal)
- You want to test it independently
For now, just know that component thinking is fundamental to modern web development. We’ll explore this deeply in Module 7: Components.
Your Project Structure
src/
├── lib/
│ └── components/
│ └── BookingForm.svelte ← New component
└── routes/
└── services/
└── [slug]/
├── +page.js
└── +page.svelte ← Uses BookingForm Common Mistakes
Wrong Import Path
<!-- ❌ Missing file extension -->
import BookingForm from '$lib/components/BookingForm';
<!-- ✅ Include .svelte extension -->
import BookingForm from '$lib/components/BookingForm.svelte'; Svelte component imports require the .svelte extension.
Component Outside src/lib
You can technically put components anywhere, but src/lib is conventional. If you create components in src/components instead, the $lib alias won’t work — you’d need relative imports.
Summary
You’ve created BookIt’s booking form as a reusable component. It lives in src/lib/components and can be imported anywhere using the $lib alias. The form renders but isn’t interactive yet.
Key takeaways:
- Components are
.sveltefiles you import and use like HTML tags - Place reusable components in
src/lib/components - Use
$libalias for clean imports
Next Steps
The form displays but doesn’t track what users type. Continue with Track Form Fields with $state to make the form reactive.