Show Users What They’re Booking

Imagine filling out a long form, clicking submit, and only then discovering you made a typo in your email or selected the wrong date. Frustrating, right? You have to go back, find the mistake, fix it, and submit again.

Now imagine instead: as you type each field, you see a preview panel that shows exactly what you’re entering. Typos are obvious immediately. Wrong dates stand out. You can fix mistakes before clicking submit, not after.

This is what live previews provide — real-time feedback that builds confidence and reduces errors.

Why This Matters for BookIt

For a booking application, mistakes are costly:

  • Wrong email? Customer never receives confirmation
  • Wrong date? They show up on the wrong day
  • Wrong time? Double-booking conflicts
  • Unclear notes? Service provider can’t prepare properly

A live preview catches these issues instantly. Users see their booking details formatted clearly as they type, giving them a chance to verify everything before submission.

What We’re Building

In this lesson, you’ll add a preview panel next to the booking form. As users fill out fields:

  • Their name appears in the preview immediately
  • Email displays as they type
  • Dates show in human-readable format (“Monday, June 15, 2025” not “2025-06-15”)
  • Times convert to 12-hour format (“2:00 PM” not “14:00”)
  • Empty fields show helpful placeholders (“Not entered” not blank space)

This is Svelte’s reactivity at its best — no manual DOM updates, no event listeners, just automatic UI updates when state changes.


What You’ll Learn

  • Display reactive state in a preview panel
  • Handle empty/incomplete states gracefully
  • Create a better user experience with real-time feedback

Add a Preview Section

Remember the debug <pre> tag we added in the last lesson to verify binding was working? Now we’ll replace it with a polished preview panel that looks professional and provides real value to users.

The Two-Column Layout

We’re going to create a side-by-side layout:

  • Left side: The booking form (what we already have)
  • Right side: Live preview panel (what we’re adding now)

This layout lets users see the form and preview simultaneously without scrolling.

Understanding the Preview Structure

The preview will use a definition list (<dl>) with term/description pairs:

<dl>
	<dt>Name</dt>
	<!-- Term: the label -->
	<dd>Jane Smith</dd>
	<!-- Description: the value -->
</dl>

This is semantic HTML for key-value pairs, and it’s perfect for showing structured data like booking details.

Adding the Preview

Replace your entire BookingForm.svelte with this updated version:

<!-- filename: src/lib/components/BookingForm.svelte -->
<script>
	let customerName = $state('')
	let email = $state('')
	let date = $state('')
	let time = $state('')
	let notes = $state('')
</script>

<div class="booking-container">
	<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" bind:value={customerName} />
		</div>

		<div class="field">
			<label for="email">Email Address</label>
			<input
				type="email"
				id="email"
				name="email"
				placeholder="jane@example.com"
				bind:value={email}
			/>
		</div>

		<div class="field">
			<label for="date">Preferred Date</label>
			<input type="date" id="date" name="date" bind:value={date} />
		</div>

		<div class="field">
			<label for="time">Preferred Time</label>
			<select id="time" name="time" bind:value={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?"
				bind:value={notes}
			></textarea>
		</div>

		<button type="submit">Request Booking</button>
	</form>

	<aside class="preview">
		<h3>Booking Preview</h3>

		<dl>
			<dt>Name</dt>
			<dd>{customerName || 'Not entered'}</dd>

			<dt>Email</dt>
			<dd>{email || 'Not entered'}</dd>

			<dt>Date</dt>
			<dd>{date || 'Not selected'}</dd>

			<dt>Time</dt>
			<dd>{time || 'Not selected'}</dd>

			<dt>Notes</dt>
			<dd>{notes || 'None'}</dd>
		</dl>
	</aside>
</div>

What Changed?

Three key additions:

1. Wrapper div:

<div class="booking-container">

This wraps both the form and preview, allowing us to style them as a flex container (side-by-side layout).

2. The preview panel:

<aside class="preview">
	<h3>Booking Preview</h3>
	<dl>...</dl>
</aside>

We use <aside> because the preview is complementary content — related to the main form but not the primary content. The <dl> (definition list) displays key-value pairs.

3. Reactive display with fallbacks:

<dd>{customerName || 'Not entered'}</dd>

This is the crucial part. Let’s break it down.

Understanding the Fallback Pattern

The expression {customerName || 'Not entered'} uses JavaScript’s logical OR operator:

  • If customerName has a value (like "Jane Smith"), it’s truthy, so display it
  • If customerName is empty (""), it’s falsy, so display 'Not entered' instead

This prevents showing blank spaces when fields are empty. Compare:

<!-- ❌ Bad: Shows blank space when empty -->
<dd>{customerName}</dd>

<!-- ✅ Good: Shows helpful placeholder -->
<dd>{customerName || 'Not entered'}</dd>

Empty strings are falsy in JavaScript, so "" || 'fallback' evaluates to 'fallback'.

Why This Works Reactively

Here’s the magic: every time users type in the name input:

  1. bind:value={customerName} updates the customerName state
  2. Svelte detects the state change
  3. Svelte re-renders the <dd>{customerName || 'Not entered'}</dd> line
  4. The preview updates instantly

You didn’t write any code to make this happen. No addEventListener, no manual DOM manipulation. Svelte’s reactivity handles it automatically.

The Preview is Just Another Use of State

Think about what we’ve built:

  • The form input is one use of customerName (via bind:value)
  • The preview display is another use of customerName (via {customerName || ...})
  • Both stay synchronized automatically because they reference the same reactive state

This is the power of declarative programming — you declare what should appear where, and Svelte figures out how to keep everything in sync.


Test the Preview

Visit a service page and start typing in the form fields. Watch the preview panel update with every keystroke.

This is Svelte’s reactivity in action:

  • Type in any field → preview updates instantly
  • Clear a field → preview shows the fallback text
  • No manual DOM updates, no event handlers needed

The same $state variable powers both bind:value (input) and {value} (display), keeping them automatically synchronized.


The following 2 sections are dedicated to formating Date and Time. These are not related to Svelte, it is only vanilla JavaScript — you can skip it.

Format the Date

Improving Date Display with vanilla JS

Right now, the date input shows the raw value like 2025-06-15. Let’s improve that by formatting it to a more user-friendly string like “Sunday, June 15, 2025”.

Add a formatting function to your <script> block:

<script>
	let customerName = $state('')
	let email = $state('')
	let date = $state('')
	let time = $state('')
	let notes = $state('')

	function formatDate(dateString) {
		if (!dateString) return 'Not selected'

		const dateObj = new Date(dateString + 'T00:00:00')
		return dateObj.toLocaleDateString('en-US', {
			weekday: 'long',
			year: 'numeric',
			month: 'long',
			day: 'numeric'
		})
	}
</script>

Understanding the Function

Let’s break down what this function does:

1. Handle empty dates:

if (!dateString) return 'Not selected'

If the user hasn’t picked a date yet (date is ""), show a friendly placeholder instead of throwing an error or showing “Invalid Date”.

2. Create a Date object:

const dateObj = new Date(dateString + 'T00:00:00')

We append 'T00:00:00' to the date string to specify midnight. Without this, JavaScript might interpret the date in UTC and display the wrong day due to timezone conversion. Adding the time ensures we get the exact date the user selected.

3. Format the date:

return dateObj.toLocaleDateString('en-US', {
	weekday: 'long', // "Sunday"
	year: 'numeric', // "2025"
	month: 'long', // "June"
	day: 'numeric' // "15"
})

The toLocaleDateString method formats dates according to locale conventions. The options object specifies:

  • weekday: 'long' — Full day name (“Sunday” not “Sun”)
  • month: 'long' — Full month name (“June” not “Jun” or “06”)
  • year and day as numbers

Using the Formatter

Now update the preview to use this function instead of displaying the raw date:

<dt>Date</dt><dd>{formatDate(date)}</dd>

Instead of:

<dd>{date || 'Not selected'}</dd>

The fallback logic (|| 'Not selected') is now inside the function, keeping the template clean.

Test the Formatted Date

Refresh the page and select a date. Instead of seeing 2025-06-15, you should see something like:

“Sunday, June 15, 2025”

Much better! This is the kind of polish that makes applications feel professional.

Why Functions for Formatting?

You might wonder: “Why not format inline in the template?”

<!-- ❌ Don't do this -->
<dd>
  {#if date}
    {new Date(date + 'T00:00:00').toLocaleDateString('en-US', {...})}
  {:else}
    Not selected
  {/if}
</dd>

This works, but:

  • The template becomes cluttered and hard to read
  • The formatting logic can’t be reused elsewhere
  • It’s harder to test
  • It mixes presentation (template) with logic (formatting)

Extracting to a function keeps templates clean and logic testable.


Format the Time

Let’s convert "14:00" to "2:00 PM". Add this function:

<script>
	// ... existing state ...

	function formatTime(timeString) {
		if (!timeString) return 'Not selected'

		const [hours, minutes] = timeString.split(':')
		const hour = parseInt(hours)
		const ampm = hour >= 12 ? 'PM' : 'AM'
		const displayHour = hour % 12 || 12

		return `${displayHour}:${minutes} ${ampm}`
	}
</script>

Using the Time Formatter

Update the preview to use this function:

<dt>Time</dt>
<dd>{formatTime(time)}</dd>

The Complete Component

Here’s the full BookingForm.svelte with preview and formatting:

<!-- filename: src/lib/components/BookingForm.svelte -->
<script>
	let customerName = $state('')
	let email = $state('')
	let date = $state('')
	let time = $state('')
	let notes = $state('')

	/**
	 * @param {string} dateString
	 */
	function formatDate(dateString) {
		if (!dateString) return 'Not selected'

		const dateObj = new Date(dateString + 'T00:00:00')
		return dateObj.toLocaleDateString('en-US', {
			weekday: 'long',
			year: 'numeric',
			month: 'long',
			day: 'numeric'
		})
	}

	/**
	 * @param {string} timeString
	 */
	function formatTime(timeString) {
		if (!timeString) return 'Not selected'

		const [hours, minutes] = timeString.split(':')
		const hour = parseInt(hours)
		const ampm = hour >= 12 ? 'PM' : 'AM'
		const displayHour = hour % 12 || 12

		return `${displayHour}:${minutes} ${ampm}`
	}
</script>

<div class="booking-container">
	<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" bind:value={customerName} />
		</div>

		<div class="field">
			<label for="email">Email Address</label>
			<input
				type="email"
				id="email"
				name="email"
				placeholder="jane@example.com"
				bind:value={email}
			/>
		</div>

		<div class="field">
			<label for="date">Preferred Date</label>
			<input type="date" id="date" name="date" bind:value={date} />
		</div>

		<div class="field">
			<label for="time">Preferred Time</label>
			<select id="time" name="time" bind:value={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?"
				bind:value={notes}
			></textarea>
		</div>

		<button type="submit">Request Booking</button>
	</form>

	<aside class="preview">
		<h3>Booking Preview</h3>

		<dl>
			<dt>Name</dt>
			<dd>{customerName || 'Not entered'}</dd>

			<dt>Email</dt>
			<dd>{email || 'Not entered'}</dd>

			<dt>Date</dt>
			<dd>{formatDate(date)}</dd>

			<dt>Time</dt>
			<dd>{formatTime(time)}</dd>

			<dt>Notes</dt>
			<dd>{notes || 'None'}</dd>
		</dl>
	</aside>
</div>

Why Live Previews ?

While live previews of form data are optional some clients may find that live previews significantly enhance user experience by providing immediate feedback and reducing errors.

With Svelte’s reactivity, this pattern requires minimal code:

<input bind:value={customerName} /><dd>{customerName || 'Not entered'}</dd>

No event listeners, no manual DOM updates. Svelte automatically synchronizes the input and display.


Common Mistakes

Forgetting Fallback Values

<!-- ❌ Shows nothing when empty -->
<dd>{customerName}</dd>

<!-- ✅ Shows placeholder -->
<dd>{customerName || 'Not entered'}</dd>

Complex Logic in Templates

<!-- ❌ Hard to read -->
<dd>
	{#if date}
		{new Date(date + 'T00:00:00').toLocaleDateString('en-US', {...})}
	{:else}
		Not selected
	{/if}
</dd>

<!-- ✅ Extract to function -->
<dd>{formatDate(date)}</dd>

Keep templates clean. Move formatting logic to functions.


Summary

The booking form now shows a live preview that updates as users type. This demonstrates Svelte’s core reactivity model:

Key Svelte concepts:

  • $state creates reactive values
  • bind:value connects inputs to state (two-way)
  • {value} displays state in templates
  • Changes to state automatically update all places where it’s used
  • No event listeners or manual DOM updates needed

What you didn’t have to write:

  • Event handlers for input changes
  • Code to update the preview
  • Synchronization logic between form and display

Svelte’s compiler generated all the reactive plumbing automatically.


Next Steps

Users can fill out the form and see a preview, but the submit button doesn’t do anything yet. Continue with Handle the Submit Button to capture form submissions.