Best Practices & Integration Guide¶
This guide covers recommended patterns for integrating with the Evocative API — from search polling to checkout, error handling, and UI implementation. All code examples use language-agnostic pseudocode unless otherwise noted.
Booking Flow Overview¶
The end-to-end booking flow follows this sequence:
flowchart TD
A[Hotel Search] --> B[User Selects Hotel]
B --> C[Room Search]
C --> D[User Selects Room]
D --> E[Checkout Page]
E --> F[User Enters Details]
F --> G[Create Order]
G --> H{Order Created}
H -->|Success| I[Process Payment]
H -->|ROOM_SOLD_OUT| J[Show Sold Out Message]
H -->|Price Changed| K[Show Price Update]
I --> L[Update Payment Status]
L --> M[Show Confirmation]
J --> N[Return to Room Selection]
K --> O{User Accepts}
O -->|Yes| I
O -->|No| N Preferred Search Flow¶
The search portion of the booking flow uses a two-step approach:
- List level —
GET /api/v2/searchreturns properties with pricing overview. - Detailed property level —
GET /api/v2/properties/{id}/packagesreturns full room and package details for a selected property.
This keeps the initial search response lightweight and defers heavier per-property detail loading to when the user actually needs it.
Session Continuity¶
The booking flow depends on maintaining session continuity through identifiers returned at each step.
| Step | Required Data |
|---|---|
| Hotel Search | Initial search parameters |
| Quote | packageId from search response |
| Book | packageBookingId from quote response |
| Payment | reservationId from order response |
Critical: Data Preservation
Store all required identifiers (packageId, packageBookingId) as users navigate through the booking flow. Loss of any identifier will require restarting the search.
Search Polling¶
Smart Polling Implementation¶
For optimal performance and user experience, implement these polling enhancements:
FUNCTION pollSearch(searchParams):
MAX_POLLS = 20 // Safety limit (~40 seconds)
POLL_INTERVAL = 2000 // 2 seconds
pollCount = 0
WHILE pollCount < MAX_POLLS:
response = CALL GET /api/v2/search WITH searchParams
// Stop when search is complete
IF response.isDone == true:
RETURN response.hotels
// Pause polling when browser tab is hidden
IF document.hidden:
WAIT until tab is visible
// Pause when user is interacting with map/filters
IF userIsInteracting:
PAUSE polling
pollCount = pollCount + 1
WAIT POLL_INTERVAL
END WHILE
// Max polls reached - show partial results
RETURN response.hotels WITH warning
END FUNCTION
Polling Best Practices¶
| Practice | Recommendation | Reason |
|---|---|---|
| Max timeout | Set 40–60 second limit | Prevents infinite polling |
| Tab visibility | Pause when hidden | Reduces unnecessary API calls |
| Request cancellation | Cancel on navigation | Prevents stale data |
| Cleanup | Cancel on component unmount | Prevents memory leaks |
Request Cancellation¶
Always implement cancellation to prevent stale requests:
// Using AbortController (JavaScript/TypeScript)
const controller = new AbortController();
fetch('/api/v2/search?' + params, { signal: controller.signal });
// Cancel when user navigates away
window.addEventListener('beforeunload', () => controller.abort());
Performance Optimization
Pausing polling when the browser tab is hidden can reduce API calls by 30–50% for users who switch tabs while waiting for results.
Progressive Results Loading¶
Don't wait for isDone=true to display results. Show hotels as they arrive:
FUNCTION handleSearchResponse(response, currentHotels):
// Merge new results with existing
updatedHotels = mergeHotels(currentHotels, response.hotels)
// Sort to maintain order
sortedHotels = sortBy(updatedHotels, currentSortCriteria)
// Update UI immediately
RENDER hotels with shimmer effect
// Show loading indicator
IF NOT response.isDone:
SHOW "Finding better prices..." indicator
ELSE:
HIDE loading indicator
REMOVE shimmer effects
END FUNCTION
Discount Validation¶
The discount field in hotel responses represents the percentage discount from the market price. While most values are accurate, implement validation to handle edge cases.
Validation Rules¶
FUNCTION validateDiscount(discount, marketPrice, price):
// Rule 1: Filter extreme outliers (data anomalies)
IF discount > 70:
RETURN null // Don't display this result or show without discount
// Rule 2: Cap displayed discount at 60%
IF discount > 60:
displayDiscount = "60%+"
ELSE:
displayDiscount = discount + "%"
// Rule 3: Verify discount matches price difference
calculatedDiscount = ((marketPrice - price) / marketPrice) * 100
IF ABS(calculatedDiscount - discount) > 5:
LOG warning "Discount mismatch for hotel"
RETURN displayDiscount
END FUNCTION
Why These Thresholds?¶
| Threshold | Action | Reason |
|---|---|---|
| > 70% | Filter out | Likely data anomaly from supplier |
| > 60% | Cap display | Prevents unrealistic expectations |
| Mismatch > 5% | Log warning | Helps identify data quality issues |
Discount Anomalies
Discounts above 70% typically indicate supplier data errors rather than genuine deals. Displaying these can lead to customer complaints when the actual savings don't match expectations.
Display Recommendations¶
IF discount > 0 AND discount <= 60:
Show: "Save {discount}%"
Show strikethrough market price
ELSE IF discount > 60 AND discount <= 70:
Show: "Save 60%+"
Show strikethrough market price
ELSE IF discount > 70:
// Don't show discount badge
// Optionally hide market price
Show only the current price
ELSE:
// No discount or invalid
Show only the current price
Market Price Display
When showing the crossed-out market price (marketPrice), consider adding a tooltip explaining: "Compared to typical rates on other travel sites" to set proper expectations.
Checkout & Order Creation¶
Handling Order Creation Responses¶
FUNCTION handleOrderResponse(response):
IF response.success == true:
// Order created successfully
STORE response.data.reservationId
STORE response.expiresAt
START payment timer (expiresAt - now)
PROCEED to payment
ELSE IF response.name == "ROOM_SOLD_OUT":
SHOW "This room is no longer available"
OFFER alternative rooms or return to search
ELSE IF response.name == "PROPERTY_NOT_FOUND":
SHOW "Hotel information has changed"
REDIRECT to hotel search
ELSE IF response.name == "OCCUPANCY_MISMATCH":
SHOW "Please provide guest details for all rooms"
HIGHLIGHT missing guest fields
ELSE:
LOG error for investigation
SHOW "Unable to complete booking. Please try again."
END FUNCTION
Preventing Duplicate Orders¶
Use the customerOrderId field to prevent duplicate bookings. If an order with the same customerOrderId already exists, the API returns DUPLICATE_CUSTOMER_ORDER_ID instead of creating a duplicate.
FUNCTION createOrderSafely(orderData):
// Generate a unique order ID for this booking attempt
customerOrderId = generateUniqueId() // e.g., UUID or your internal order ID
orderData.customerOrderId = customerOrderId
// Prevent double-submission on UI level
IF orderInProgress:
RETURN // Ignore duplicate clicks
orderInProgress = true
DISABLE submit button
SHOW loading state
TRY:
response = CALL POST /api/order WITH orderData
IF response.success:
STORE response.data.reservationId
PROCEED to payment
ELSE IF response.name == "DUPLICATE_CUSTOMER_ORDER_ID":
// Order already exists - safe to retry
SHOW "Order already created"
FETCH existing order status
ELSE:
HANDLE error response
CATCH networkError:
// Safe to retry with same customerOrderId
SHOW "Connection error. Click to retry."
// Retrying will either succeed or return DUPLICATE_CUSTOMER_ORDER_ID
FINALLY:
orderInProgress = false
ENABLE submit button
END FUNCTION
Idempotency with customerOrderId
By including a unique customerOrderId with each booking request, you can safely retry failed requests without risk of creating duplicate bookings. The same customerOrderId will always map to the same order. It is an optional string that must be unique (or null); use a collision-proof value such as a prefix + GUID (e.g. <company_prefix>_<guid>).
Handling Room Unavailability¶
No Pre-Booking Check
The API does not provide a pre-booking validation endpoint. Room availability and pricing are confirmed only when creating an order. If a room becomes unavailable between search and checkout, the order creation will return a ROOM_SOLD_OUT error.
When order creation fails with ROOM_SOLD_OUT, the room is no longer available at the originally displayed price. This can occur due to:
- Room inventory exhausted
- Dynamic pricing changes
- Supplier rate expiration
Sold Out Recovery¶
When a room becomes unavailable, provide a smooth recovery experience:
FUNCTION handleSoldOut(evocativeId, originalRoom):
// Fetch alternative rooms
alternatives = CALL /api/v2/search WITH evocativeId
IF alternatives.length > 0:
// Find similar rooms
similarRooms = FILTER alternatives WHERE:
- sameRoomType OR
- priceDifference < 20% OR
- sameCancellationPolicy
IF similarRooms.length > 0:
SHOW "Your room is no longer available. Here are similar options:"
DISPLAY similarRooms
ELSE:
SHOW "Your room is no longer available. Other rooms at this hotel:"
DISPLAY alternatives
ELSE:
SHOW "No rooms available at this hotel"
OFFER to return to hotel search
END FUNCTION
Payment Handling¶
Payment Timer Management¶
Orders expire if payment isn't completed within the time limit (typically 20 minutes). Implement a countdown timer to keep users informed.
FUNCTION startPaymentTimer(expiresAt):
remainingTime = expiresAt - currentTime
WHILE remainingTime > 0:
DISPLAY formatTime(remainingTime)
// Warning thresholds
IF remainingTime <= 60 seconds:
SHOW urgent warning "Less than 1 minute remaining!"
HIGHLIGHT timer in red
ELSE IF remainingTime <= 300 seconds:
SHOW warning "5 minutes remaining"
HIGHLIGHT timer in orange
WAIT 1 second
remainingTime = remainingTime - 1
// Timer expired
SHOW "Your reservation has expired"
DISABLE payment button
OFFER to start new booking
END FUNCTION
Payment Expiration
Always display the remaining time to users. If the timer expires before payment is confirmed, the reservation is automatically released and users must start over.
Confirmation Best Practices¶
After successful payment confirmation:
- Display confirmation immediately — Show reservationId and booking details
- Send confirmation email — Include all booking details and hotel contact
- Provide save/print option — Allow users to save confirmation offline
- Show next steps — Check-in time, hotel address, cancellation policy
FUNCTION showConfirmation(orderStatus):
DISPLAY:
- Booking reference: orderStatus.bookingReference
- Hotel: orderStatus.hotelName
- Address: orderStatus.hotelAddress
- Check-in: formatDate(orderStatus.checkInDate)
- Check-out: formatDate(orderStatus.checkOutDate)
- Room: orderStatus.roomName
- Total paid: formatCurrency(orderStatus.price)
- Cancellation policy summary
OFFER:
- "Add to Calendar" button
- "Print Confirmation" button
- "Email Confirmation" button
END FUNCTION
Error Handling & Recovery¶
Error Code Reference¶
| Code | Error | Cause | Recovery Action |
|---|---|---|---|
| 400 | Bad Request | Invalid parameters or malformed request | Validate input before request; show user-friendly error |
| 401 | Unauthorized | Missing or invalid API key | Check API key configuration |
| 404 | Not Found | Resource not found | Check request parameters |
| 429 | Too Many Requests | Rate limit exceeded | Implement exponential backoff; retry after delay |
| 500 | Server Error | Internal server error | Retry with backoff; show error after 3 attempts |
| 502 | Bad Gateway | Request timed out or upstream failure | Search: re-issue with identical parameters to recover the in-progress result (retained 5 min). Order/booking: retry safely using the same customerOrderId idempotency key (see Preventing Duplicate Orders). Then backoff |
| 503 | Service Unavailable | Service temporarily down | Retry with backoff; show maintenance message |
Retry Logic with Exponential Backoff¶
FUNCTION fetchWithRetry(url, options, maxRetries = 3):
retryCount = 0
baseDelay = 1000 // 1 second
WHILE retryCount < maxRetries:
TRY:
response = FETCH url WITH options
IF response.status == 200:
RETURN response.data
// Don't retry client errors (except rate limiting)
IF response.status >= 400 AND response.status < 500:
IF response.status == 429:
// Rate limited - use longer delay
delay = getRetryAfterHeader(response) OR (baseDelay * 4)
WAIT delay
retryCount = retryCount + 1
CONTINUE
ELSE:
THROW ClientError(response.status, response.body)
// Server errors - retry with backoff
IF response.status >= 500:
delay = baseDelay * (2 ^ retryCount) // Exponential: 1s, 2s, 4s
WAIT delay
retryCount = retryCount + 1
CATCH networkError:
// Network failures - retry with backoff
delay = baseDelay * (2 ^ retryCount)
WAIT delay
retryCount = retryCount + 1
END WHILE
THROW MaxRetriesExceeded
END FUNCTION
Handling Specific Scenarios¶
Rate Limiting (429)¶
FUNCTION handleRateLimit(response):
// Check for Retry-After header
retryAfter = response.headers["Retry-After"]
IF retryAfter EXISTS:
WAIT retryAfter seconds
ELSE:
WAIT 5 seconds // Default backoff
// Reduce polling frequency temporarily
INCREASE pollInterval BY 50%
RETRY request
END FUNCTION
Rate Limit Prevention
To avoid hitting rate limits:
- Use 2-second polling intervals (not faster)
- Pause polling when browser tab is hidden
- Cancel requests when user navigates away
- Don't start new searches while one is in progress
Handling Expired Sessions¶
When a booking session expires, the API automatically performs a new search with the same parameters. If the original room offer cannot be found, you'll receive a ROOM_SOLD_OUT error at order creation time.
flowchart TD
A[Create Order Request] --> B{Session Valid}
B -->|Yes| C[Process Order]
B -->|No| D[Auto Re-search]
D --> E{Room Found}
E -->|Yes| C
E -->|No| F[Return ROOM_SOLD_OUT]
F --> G[Offer Alternative Rooms] No Explicit 404 for Sessions
The API does not return a 404 error specifically for expired sessions. Handle ROOM_SOLD_OUT errors by offering alternative rooms to the user.
Gateway Errors (502/503)¶
FUNCTION handleGatewayError(error, currentPoll):
IF currentPoll.retryCount < 2:
// Quick retry for transient errors
WAIT 500ms
RETRY request
ELSE:
// Show partial results if available
IF currentPoll.hasPartialResults:
SHOW results WITH warning "Some results may be missing"
ELSE:
SHOW error "Unable to load results. Please try again."
END FUNCTION
Order Error Recovery¶
FUNCTION handleOrderError(error, context):
SWITCH error.status:
CASE 400:
// Bad request - validation error
SHOW error.message
HIGHLIGHT invalid fields
CASE 404:
// Session expired or resource not found
IF context.hasEs:
SHOW "Your session has expired"
REDIRECT to search with preserved criteria
ELSE:
SHOW "Booking information not found"
REDIRECT to home
CASE 429:
// Rate limited
WAIT 5 seconds
RETRY with backoff
CASE 500, 502, 503:
// Server error
IF retryCount < 2:
WAIT 2 seconds
RETRY
ELSE:
SHOW "Service temporarily unavailable"
OFFER manual retry button
DEFAULT:
LOG error
SHOW "Something went wrong. Please try again."
END FUNCTION
User-Facing Error Messages¶
| Error Type | User Message | Action Button |
|---|---|---|
| Network error | "Connection lost. Please check your internet." | "Retry" |
| Rate limited | "Too many requests. Please wait a moment." | Auto-retry (hidden) |
| Session expired | "Your session has expired." | "Search Again" |
| Server error | "Something went wrong. Please try again." | "Retry" |
| No results | "No hotels found matching your criteria." | "Modify Search" |
Error Recovery UX
Always preserve the user's search criteria when redirecting after errors. This allows them to quickly retry without re-entering dates, guests, and destination.
UI Patterns¶
Loading States¶
During search polling, prices arrive progressively. Use visual states to communicate loading progress:
stateDiagram-v2
[*] --> Skeleton: Search starts
Skeleton --> Shimmer: First results arrive
Shimmer --> FinalPrice: isFinished=true
Shimmer --> Shimmer: New prices arrive
FinalPrice --> [*] | State | When | Visual Treatment |
|---|---|---|
| Skeleton | No data yet | Gray placeholder blocks |
| Shimmer | Partial data, still polling | Show price with animated shimmer overlay |
| Final | isDone=true | Solid price, no animation |
Skeleton Implementation¶
Display placeholder content while waiting for initial results:
COMPONENT PriceSkeleton:
RENDER:
<div class="skeleton">
<div class="skeleton-line" width="60px" /> // Price
<div class="skeleton-line" width="40px" /> // Per night
<div class="skeleton-line" width="80px" /> // Taxes text
</div>
CSS Pattern:
.skeleton {
background: linear-gradient(
90deg,
#e0e0e0 25%,
#f0f0f0 50%,
#e0e0e0 75%
);
background-size: 200% 100%;
animation: skeleton-pulse 1.5s ease-in-out infinite;
border-radius: 4px;
}
@keyframes skeleton-pulse {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Shimmer Overlay¶
When prices are loaded but search is still polling, add a shimmer effect to indicate potential updates:
.price-shimmer {
position: relative;
overflow: hidden;
}
.price-shimmer::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.4) 50%,
transparent 100%
);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
Implementation Pattern¶
FUNCTION renderPrice(hotel, isFinished):
IF hotel.price == null:
RETURN <PriceSkeleton />
IF NOT isFinished:
RETURN <PriceDisplay class="shimmer" price={hotel.price} />
RETURN <PriceDisplay price={hotel.price} />
END FUNCTION
Button States¶
stateDiagram-v2
[*] --> Disabled: Form incomplete
Disabled --> Ready: Form valid
Ready --> Loading: User clicks
Loading --> Ready: Search complete
Loading --> Error: Search failed
Error --> Ready: User corrects | State | Appearance | Behavior |
|---|---|---|
| Disabled | Grayed out, reduced opacity | No click response |
| Ready | Primary color, full opacity | Clickable |
| Loading | Primary color + spinner | Click disabled, shows progress |
| Error | Red/warning color | Shows error message |
.button {
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
transition: all 0.2s ease;
cursor: pointer;
}
.button--disabled {
background: #ccc;
color: #666;
cursor: not-allowed;
opacity: 0.6;
}
.button--ready {
background: #007bff;
color: white;
}
.button--ready:hover {
background: #0056b3;
transform: translateY(-1px);
}
.button--loading {
background: #007bff;
color: white;
cursor: wait;
pointer-events: none;
}
.button--loading::after {
content: '';
width: 16px;
height: 16px;
border: 2px solid white;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-left: 8px;
display: inline-block;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
Checkout Button Logic¶
FUNCTION renderCheckoutButton(formState, orderState):
IF NOT formState.isValid:
RETURN <Button disabled>Complete required fields</Button>
IF orderState.isSubmitting:
RETURN <Button loading>Processing...</Button>
IF orderState.error:
RETURN <Button error onClick={retry}>Try Again</Button>
RETURN <Button ready onClick={submit}>Complete Booking</Button>
END FUNCTION
Price Display¶
COMPONENT PriceDisplay:
PROPS:
- totalPrice: number
- pricePerNight: number
- marketPrice: number (optional)
- discount: number (optional)
- nights: number
- showTaxesIncluded: boolean
RENDER:
<div class="price-container">
// Market price (crossed out)
IF marketPrice AND discount > 0 AND discount <= 70:
<span class="market-price">{formatCurrency(marketPrice)}</span>
// Current price
<span class="current-price">{formatCurrency(totalPrice)}</span>
// Discount badge
IF discount > 0 AND discount <= 60:
<span class="discount-badge">Save {discount}%</span>
ELSE IF discount > 60 AND discount <= 70:
<span class="discount-badge">Save 60%+</span>
// Per night breakdown
IF nights > 1:
<span class="per-night">
{formatCurrency(pricePerNight)} / night
</span>
// Tax indicator
IF showTaxesIncluded:
<span class="taxes-included">
<CheckIcon /> Taxes included
</span>
</div>
.price-container {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.market-price {
color: #666;
text-decoration: line-through;
font-size: 14px;
}
.current-price {
color: #1a1a1a;
font-size: 24px;
font-weight: 600;
}
.discount-badge {
background: #e8f5e9;
color: #2e7d32;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.per-night {
color: #666;
font-size: 13px;
}
.taxes-included {
color: #2e7d32;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
Error Display¶
| Error Type | Display Location | Style |
|---|---|---|
| Form validation | Below input field | Inline, red text |
| API error | Toast/banner | Dismissible notification |
| Session expired | Modal dialog | Blocking, requires action |
| Network error | Toast + retry | Non-blocking with action |
.error-inline {
color: #d32f2f;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
margin-top: 4px;
}
.error-toast {
position: fixed;
bottom: 20px;
right: 20px;
background: #ffebee;
border: 1px solid #ef9a9a;
border-radius: 8px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slide-in 0.3s ease;
}
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
Form Validation¶
Validate fields as users type, but delay error display:
FUNCTION handleFieldChange(field, value):
// Validate immediately
validationResult = validate(field, value)
// Store validation state
SET fieldState.isValid = validationResult.isValid
SET fieldState.error = validationResult.error
// Delay showing error (don't interrupt typing)
IF NOT validationResult.isValid:
DEBOUNCE 500ms:
IF field.hasBeenBlurred:
SHOW error message
ELSE:
HIDE error message immediately
END FUNCTION
Guest Details Validation¶
VALIDATION_RULES:
firstName:
- required: "First name is required"
- maxLength(50): "Maximum 50 characters"
lastName:
- required: "Last name is required"
- maxLength(50): "Maximum 50 characters"
email:
- required: "Email is required"
- email: "Please enter a valid email"
- maxLength(200): "Maximum 200 characters"
phone:
- required: "Phone number is required"
- pattern(/^\+?[\d\s-]+$/): "Please enter a valid phone number"
Payment Timer UI¶
COMPONENT PaymentTimer:
PROPS:
- expiresAt: timestamp
STATE:
- remainingSeconds: number
- urgency: 'normal' | 'warning' | 'critical'
ON_MOUNT:
START interval every 1 second:
remainingSeconds = calculateRemaining(expiresAt)
IF remainingSeconds <= 0:
urgency = 'expired'
TRIGGER onExpired callback
ELSE IF remainingSeconds <= 60:
urgency = 'critical'
ELSE IF remainingSeconds <= 300:
urgency = 'warning'
ELSE:
urgency = 'normal'
RENDER:
<div class="timer timer--{urgency}">
<ClockIcon />
<span>
{formatTime(remainingSeconds)} remaining
</span>
</div>
.timer {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
}
.timer--normal {
background: #e3f2fd;
color: #1565c0;
}
.timer--warning {
background: #fff3e0;
color: #e65100;
animation: pulse 2s infinite;
}
.timer--critical {
background: #ffebee;
color: #c62828;
animation: pulse 1s infinite;
}
.timer--expired {
background: #f5f5f5;
color: #666;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
Mobile Considerations¶
Touch-Friendly Targets¶
Ensure interactive elements are easily tappable:
/* Minimum touch target size: 44x44px */
.button,
.card-clickable,
.input {
min-height: 44px;
min-width: 44px;
}
/* Add padding for small text links */
.link {
padding: 8px;
margin: -8px;
}
Responsive Price Display¶
.price-container {
/* Desktop */
align-items: flex-end;
text-align: right;
}
@media (max-width: 768px) {
.price-container {
/* Mobile */
align-items: flex-start;
text-align: left;
}
.current-price {
font-size: 20px;
}
.market-price,
.per-night {
font-size: 12px;
}
}
Accessibility¶
Screen Reader Announcements¶
Announce dynamic content changes:
FUNCTION announceToScreenReader(message):
// Create or reuse live region
liveRegion = document.getElementById('sr-announcements')
IF NOT liveRegion:
liveRegion = CREATE element 'div'
SET aria-live = "polite"
SET aria-atomic = "true"
SET class = "sr-only"
APPEND to body
// Update content (triggers announcement)
liveRegion.textContent = message
END FUNCTION
// Usage
ON search complete:
announceToScreenReader("{count} hotels found")
ON price update:
announceToScreenReader("Price updated to {price}")
ON error:
announceToScreenReader("Error: {message}")
Focus Management¶
FUNCTION handleModalOpen(modalElement):
// Store current focus
previousFocus = document.activeElement
// Move focus to modal
modalElement.focus()
// Trap focus within modal
ENABLE focus trap
ON modal close:
// Restore focus
previousFocus.focus()
Screen Reader Only CSS¶
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Integration Checklist¶
Before launching your booking integration, verify:
- Search polling: 2-second intervals with max timeout and tab visibility handling
- Progressive loading: Results appear as they arrive, not only after
isFinished - Discount validation: Outliers filtered (>70%), display capped (>60%)
- Session continuity: All identifiers (
packageId,packageBookingId) preserved across steps - Duplicate prevention:
customerOrderIdincluded in all order requests - Payment timer: Countdown visible with urgency indicators
- Error handling: Retry logic with exponential backoff, user-friendly messages
- Sold out recovery: Alternative rooms offered when
ROOM_SOLD_OUToccurs - Loading states: Skeleton → Shimmer → Final transitions work smoothly
- Button states: Disabled, loading, error states are visually distinct
- Form validation: Real-time feedback without interrupting users
- Mobile: Touch targets are 44px+, layout adapts to small screens
- Accessibility: Screen readers announce dynamic changes