Modals

Modals provide focused, contextual interfaces for specific tasks or information. They follow the same spacing hierarchy as cards, maintaining consistency across the design system while offering flexibility for future multi-column layouts.

Principles

Spacing Consistency

  • Same as Cards: Modals use identical spacing hierarchy as cards
  • Text → Highlight → Modal: Follow the text + 20px + 20px formula
  • Responsive Width: Single-column modals scale with viewport
  • Future-Ready: System accommodates multi-column modals

User Experience

  • Focus & Context: Modals isolate specific tasks or information
  • Smooth Animations: Slide-down entrance with fade overlay
  • Easy Dismissal: Close button, ESC key, or click outside
  • Scroll Behavior: Vertical scroll within modal when needed

Single-Column Modal System

Width Formula

Single-column modals follow the spacing hierarchy formula: text content + 40px padding (20px each side). The modal width scales fluidly with the viewport between defined minimum and maximum values.

Width Calculation
  • At 360px viewport: 320px modal (280px text + 40px)
  • At 425px viewport: 385px modal (345px text + 40px)
  • Formula: (viewport width - 40px)
  • Range: Clamped between 320px and 385px
Spacing Breakdown
  • Text content: x pixels
  • Highlight boxes: x + 20px (10px each side)
  • Modal container: x + 40px (20px each side)
  • Viewport margins: 20px on each side
/* Single-column modal CSS */
.modal-content {
  width: clamp(
    var(--modal-single-col-min-width),  /* 320px */
    calc(100vw - 40px),                  /* Fluid with viewport */
    var(--modal-single-col-max-width)    /* 385px */
  );
  max-height: calc(100vh - 80px - 40px);
  overflow-y: auto;
  padding: 20px 20px 30px 20px;
}

Positioning & Layout

Vertical Positioning

  • Top offset: 80px from viewport top
  • Bottom clearance: 40px from viewport bottom
  • Max height: calc(100vh - 120px)
  • Overflow: Vertical scroll when content exceeds height

Horizontal Positioning

  • Alignment: Horizontally centered
  • Side margins: Calculated automatically (20px minimum)
  • Width behavior: Grows with viewport until max
  • Responsive: Maintains readability at all sizes

Animation & Interaction

Animation System

Modals use smooth, purposeful animations that guide the user's attention without being distracting. The combination of fade and slide creates a sense of the modal coming into view naturally.

Entrance
  • Overlay: Fade in (0 → 100% opacity)
  • Modal: Slide down 20px + fade in
  • Duration: 300ms
  • Easing: cubic-bezier(0.16, 1, 0.3, 1)
Exit
  • Modal: Slide up 20px + fade out
  • Overlay: Fade out (100% → 0 opacity)
  • Duration: 300ms
  • Cleanup: Remove from DOM after animation

Dismissal Methods

  • Close Button: X button in top-right corner (32x32px touch target)
  • ESC Key: Keyboard shortcut for accessibility
  • Click Outside: Click on overlay dismisses modal
  • Programmatic: Modal can close itself after successful action

Future: Multi-Column Modals

Planning for Complexity

While current modals use a single column, the system is designed to accommodate more complex layouts in the future. Multi-column modals will follow the same spacing principles scaled to the appropriate layout context.

Two-Column Modals
  • Available on medium and large layouts only
  • Width based on 2 columns + gap + padding
  • Useful for comparison or side-by-side content
  • Still maintains text/highlight/modal hierarchy
Three-Column Modals
  • Available on large layouts only (1005px+)
  • Width based on 3 columns + gaps + padding
  • Reserved for complex workflows or dashboards
  • Always single-column on small layouts
/* Future multi-column modal classes */
.modal-content--two-col {
  /* Calculated based on medium/large layout columns */
}

.modal-content--three-col {
  /* Calculated based on large layout columns */
}

Implementation

Using the Modal Component

The Modal component is located at [`@/components/ui/Modal.tsx`](src/components/ui/Modal.tsx:1) and handles all the positioning, animation, and interaction logic automatically.

import { Modal, ModalCloseButton } from '@/components/ui/Modal'

function MyComponent() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Open Modal
      </button>

      <Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
        <div className="relative">
          <ModalCloseButton 
            onClose={() => setIsOpen(false)} 
            className="absolute top-2 right-2" 
          />
          
          <h2 className="text-h2">Modal Title</h2>
          <p className="text-body">Modal content goes here...</p>
        </div>
      </Modal>
    </>
  )
}