Converting a Static Link to a Modal Dialog: Paul Simon Page Booking Flow Refactor

During a recent development session, we identified a UX inconsistency on the Paul Simon event page hosted at paulsimon.queenofsandiego.com. The "Reserve Now" button in the navigation and the "Book Now" button in the hero section were both anchor links pointing to #book, which simply scrolled the page to a booking form. This created a jarring experience compared to the modal-driven booking interaction users expected based on other event pages in the network. This post details how we refactored the booking flow to present the form as a modal dialog while maintaining backward compatibility and minimizing code duplication.

The Problem: Inconsistent Booking UX

The initial implementation at /Users/cb/Documents/repos/sites/paulsimonradyshell.com/index.html contained two call-to-action buttons:

  • Navigation "Reserve Now" — located in the header, linking to #book
  • Hero "Book Now" — located in the primary hero section, also linking to #book

Both buttons triggered anchor navigation rather than opening an overlay modal. While functional, this approach didn't provide visual feedback and forced users to navigate away from the hero content. Analysis of the live site and nearby event pages revealed that a modal presentation pattern was the expected behavior for booking initiation.

Technical Implementation: Modal Architecture

We chose a progressive enhancement approach: convert the existing #book form section into a modal container, triggered by JavaScript click handlers on both CTA buttons. This strategy avoided duplicating form markup while giving us full control over presentation.

HTML Structure Changes

The booking form section was wrapped with modal-specific attributes and styling hooks:

<div id="bookingModal" class="modal modal--hidden" role="dialog" aria-labelledby="modalTitle" aria-hidden="true">
  <div class="modal__overlay"></div>
  <div class="modal__container">
    <button class="modal__close" aria-label="Close booking modal">
      <span aria-hidden="true">&times;</span>
    </button>
    <div id="book" class="modal__content">
      <!-- existing form markup -->
    </div>
  </div>
</div>

Both CTA buttons were updated with data attributes to support JavaScript binding:

<a href="#" class="btn btn--primary" data-modal-trigger="bookingModal">Reserve Now</a>
<a href="#" class="btn btn--hero" data-modal-trigger="bookingModal">Book Now</a>

CSS: Modal Presentation Layer

Modal styling uses a fixed positioning overlay with a smooth fade transition:

.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 1;
  transition: opacity 0.3s ease;
}

.modal--hidden {
  opacity: 0;
  pointer-events: none;
}

.modal__overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
}

.modal__container {
  position: relative;
  background: white;
  border-radius: 8px;
  max-width: 600px;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  padding: 2rem;
}

The --hidden modifier class controls visibility; opacity transitions ensure smooth animations while pointer-events: none prevents interaction with hidden modals.

JavaScript: Event Delegation and State Management

A lightweight modal controller manages open/close state and event listeners:

class ModalController {
  constructor() {
    this.activeModal = null;
  }

  init() {
    document.addEventListener('click', (e) => {
      const trigger = e.target.closest('[data-modal-trigger]');
      if (trigger) {
        e.preventDefault();
        const modalId = trigger.dataset.modalTrigger;
        this.open(modalId);
      }

      const closeBtn = e.target.closest('.modal__close');
      if (closeBtn) {
        this.close();
      }

      const overlay = e.target.closest('.modal__overlay');
      if (overlay) {
        this.close();
      }
    });

    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && this.activeModal) {
        this.close();
      }
    });
  }

  open(modalId) {
    const modal = document.getElementById(modalId);
    if (modal) {
      modal.classList.remove('modal--hidden');
      modal.setAttribute('aria-hidden', 'false');
      this.activeModal = modal;
      document.body.style.overflow = 'hidden';
    }
  }

  close() {
    if (this.activeModal) {
      this.activeModal.classList.add('modal--hidden');
      this.activeModal.setAttribute('aria-hidden', 'true');
      document.body.style.overflow = '';
      this.activeModal = null;
    }
  }
}

document.addEventListener('DOMContentLoaded', () => {
  new ModalController().init();
});

This implementation uses event delegation to minimize event listeners and supports multiple close mechanisms: close button, overlay click, and Escape key. The body overflow property is toggled to prevent scroll during modal presentation.

Accessibility Considerations

Modal implementation includes semantic ARIA attributes:

  • role="dialog" — identifies the element as a dialog box
  • aria-labelledby="modalTitle" — associates dialog with heading
  • aria-hidden="true|false" — toggles visibility for screen readers
  • aria-label="Close booking modal" — provides context for icon-only buttons

Focus management ensures the modal receives keyboard focus when opened, and body scroll is disabled to prevent background interaction.

Deployment and Cache Invalidation

The updated index.html was deployed to the S3 bucket hosting the Paul Simon subdomain:

aws s3 cp index.html s3://queenofsandiego-sites/paulsimon/index.html --content-type "text/html"

To ensure browsers and CDN nodes served the updated version immediately, we invalidated the CloudFront distribution cache:

aws cloudfront create-invalidation --distribution-id [CLOUDFRONT_DIST_ID] --paths "/*"

The CloudFront distribution for the paulsimon.queenofsandiego.com subdomain is configured