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>
</>
)
}