Modals
Modals are overlay dialogs that require user interaction before returning to the main content. They focus attention on critical information, decisions, or forms.
Installation
Copy the modal CSS from styles/docs.css or include the Standard stylesheet:
<link rel="stylesheet" href="standard.min.css">
Basic modal structure:
<div class="Modal-overlay">
<div class="Modal">
<header class="Modal-header">
<h2 class="Modal-title">Modal Title</h2>
<button class="Button Button--icon Button--ghost" aria-label="Close">
<i class="ph ph-x"></i>
</button>
</header>
<div class="Modal-body">
<p>Modal content goes here.</p>
</div>
<footer class="Modal-footer">
<button class="Button Button--tertiary">Cancel</button>
<button class="Button Button--primary">Confirm</button>
</footer>
</div>
</div>
Usage
Modals use a two-part structure: .Modal-overlay for the backdrop and .Modal for the dialog itself. Add .active or .Modal-overlay--active to show the modal.
Example Modal
This is an example modal dialog.
<div style="position: relative; width: 100%; height: 200px; background: var(--bg-s); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center;">
<div class="Modal" style="position: relative; transform: none; max-width: 320px;">
<header class="Modal-header">
<h2 class="Modal-title">Example Modal</h2>
<button class="Button Button--icon Button--ghost" aria-label="Close">
<i class="ph ph-x"></i>
</button>
</header>
<div class="Modal-body">
<p>This is an example modal dialog.</p>
</div>
<footer class="Modal-footer">
<button class="Button Button--tertiary">Cancel</button>
<button class="Button Button--primary">Confirm</button>
</footer>
</div>
</div>
Examples
Basic
A simple modal with header, body, and footer actions.
Save Changes?
You have unsaved changes. Would you like to save them before leaving?
<div style="position: relative; width: 100%; height: 220px; background: oklch(0% 0 0 / 0.5); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center; padding: var(--space-4);">
<div class="Modal" style="position: relative; transform: none;">
<header class="Modal-header">
<h2 class="Modal-title">Save Changes?</h2>
<button class="Button Button--icon Button--ghost" aria-label="Close">
<i class="ph ph-x"></i>
</button>
</header>
<div class="Modal-body">
<p>You have unsaved changes. Would you like to save them before leaving?</p>
</div>
<footer class="Modal-footer">
<button class="Button Button--tertiary">Discard</button>
<button class="Button Button--primary">Save</button>
</footer>
</div>
</div>
Confirmation
Use danger buttons for destructive actions. Clear messaging helps users understand consequences.
Delete Project?
This will permanently delete "My Project" and all associated files. This action cannot be undone.
<div style="position: relative; width: 100%; height: 240px; background: oklch(0% 0 0 / 0.5); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center; padding: var(--space-4);">
<div class="Modal Modal--small" style="position: relative; transform: none;">
<header class="Modal-header">
<h2 class="Modal-title">Delete Project?</h2>
<button class="Button Button--icon Button--ghost" aria-label="Close">
<i class="ph ph-x"></i>
</button>
</header>
<div class="Modal-body">
<p>This will permanently delete <strong>"My Project"</strong> and all associated files. This action cannot be undone.</p>
</div>
<footer class="Modal-footer">
<button class="Button Button--secondary">Cancel</button>
<button class="Button Button--danger">Delete Project</button>
</footer>
</div>
</div>
Form Modal
Modals work great for focused form inputs. Keep forms short — complex flows belong on dedicated pages.
Create New Workspace
<div style="position: relative; width: 100%; height: 360px; background: oklch(0% 0 0 / 0.5); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center; padding: var(--space-4);">
<div class="Modal" style="position: relative; transform: none;">
<header class="Modal-header">
<h2 class="Modal-title">Create New Workspace</h2>
<button class="Button Button--icon Button--ghost" aria-label="Close">
<i class="ph ph-x"></i>
</button>
</header>
<div class="Modal-body">
<div class="FormField" style="margin-bottom: var(--space-4);">
<label class="FormField-label">Workspace Name</label>
<input type="text" class="Input" placeholder="My Workspace">
</div>
<div class="FormField">
<label class="FormField-label">Description</label>
<input type="text" class="Input" placeholder="Optional description...">
</div>
</div>
<footer class="Modal-footer">
<button class="Button Button--tertiary">Cancel</button>
<button class="Button Button--primary">Create Workspace</button>
</footer>
</div>
</div>
Fullscreen
Fullscreen modals take over the entire viewport. Use for immersive experiences or complex multi-step flows.
Document Editor
Fullscreen content area
<div style="position: relative; width: 100%; height: 300px; background: oklch(0% 0 0 / 0.5); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center;">
<div class="Modal Modal--fullscreen" style="position: relative; transform: none; height: 100%; border-radius: var(--r-m);">
<header class="Modal-header">
<h2 class="Modal-title">Document Editor</h2>
<button class="Button Button--icon Button--ghost" aria-label="Close">
<i class="ph ph-x"></i>
</button>
</header>
<div class="Modal-body" style="display: flex; align-items: center; justify-content: center; color: var(--fg-3);">
<p>Fullscreen content area</p>
</div>
<footer class="Modal-footer">
<button class="Button Button--tertiary">Cancel</button>
<button class="Button Button--primary">Save Document</button>
</footer>
</div>
</div>
Slideout / Drawer
Slide-in panels from the side work well for settings, filters, or detailed information.
<div style="position: relative; width: 100%; height: 280px; background: oklch(0% 0 0 / 0.5); border-radius: var(--r-m); display: flex; justify-content: flex-end; overflow: hidden;">
<div class="Modal" style="position: relative; transform: none; height: 100%; border-radius: 0; max-width: 320px; border-left: 1px solid var(--bd);">
<header class="Modal-header">
<h2 class="Modal-title">Filters</h2>
<button class="Button Button--icon Button--ghost" aria-label="Close">
<i class="ph ph-x"></i>
</button>
</header>
<div class="Modal-body">
<div class="FormField" style="margin-bottom: var(--space-4);">
<label class="FormField-label">Category</label>
<select class="Select">
<option>All Categories</option>
<option>Design</option>
<option>Development</option>
</select>
</div>
<div class="FormField">
<label class="FormField-label">Status</label>
<select class="Select">
<option>Any Status</option>
<option>Active</option>
<option>Archived</option>
</select>
</div>
</div>
<footer class="Modal-footer">
<button class="Button Button--tertiary">Reset</button>
<button class="Button Button--primary">Apply Filters</button>
</footer>
</div>
</div>
Nested
Open a modal from within another modal. The second modal uses .Modal-overlay--nested for higher z-index.
Edit Profile
Primary modal content...
Discard Changes?
You have unsaved changes. Are you sure you want to close?
<div style="position: relative; width: 100%; height: 320px; background: oklch(0% 0 0 / 0.5); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center; padding: var(--space-4);">
<div class="Modal" style="position: relative; transform: none; opacity: 0.6; scale: 0.95;">
<header class="Modal-header">
<h2 class="Modal-title">Edit Profile</h2>
</header>
<div class="Modal-body">
<p>Primary modal content...</p>
</div>
</div>
<div class="Modal Modal--small" style="position: absolute; transform: none;">
<header class="Modal-header">
<h2 class="Modal-title">Discard Changes?</h2>
<button class="Button Button--icon Button--ghost" aria-label="Close">
<i class="ph ph-x"></i>
</button>
</header>
<div class="Modal-body">
<p>You have unsaved changes. Are you sure you want to close?</p>
</div>
<footer class="Modal-footer">
<button class="Button Button--tertiary">Keep Editing</button>
<button class="Button Button--danger">Discard</button>
</footer>
</div>
</div>
Image Lightbox
Display images in a focused, centered view with minimal chrome.
<div style="position: relative; width: 100%; height: 280px; background: oklch(0% 0 0 / 0.85); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center; padding: var(--space-4);">
<button class="Button Button--icon Button--ghost" style="position: absolute; top: var(--space-3); right: var(--space-3); color: white;" aria-label="Close">
<i class="ph ph-x"></i>
</button>
<img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=200&fit=crop" alt="Mountain landscape" style="max-width: 100%; max-height: 220px; border-radius: var(--r-m); object-fit: cover;">
<button class="Button Button--icon Button--ghost" style="position: absolute; left: var(--space-3); color: white;" aria-label="Previous">
<i class="ph ph-caret-left"></i>
</button>
<button class="Button Button--icon Button--ghost" style="position: absolute; right: var(--space-3); color: white;" aria-label="Next">
<i class="ph ph-caret-right"></i>
</button>
</div>
Video Modal
Embed video content with playback controls.
<div style="position: relative; width: 100%; height: 280px; background: oklch(0% 0 0 / 0.85); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center; padding: var(--space-6);">
<button class="Button Button--icon Button--ghost" style="position: absolute; top: var(--space-3); right: var(--space-3); color: white;" aria-label="Close">
<i class="ph ph-x"></i>
</button>
<div style="width: 100%; max-width: 480px; aspect-ratio: 16/9; background: oklch(10% 0 0); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center;">
<button class="Button Button--icon Button--primary Button--large" aria-label="Play video">
<i class="ph ph-play-fill"></i>
</button>
</div>
</div>
Sizes
Modals come in three sizes: small, medium (default), and large. Fullscreen is also available.
Small (360px)
Compact dialogs
Medium (480px) — Default
Standard modal width
Large (720px)
For complex content or forms
<div style="display: flex; flex-direction: column; gap: var(--space-4); width: 100%;">
<div class="Modal Modal--small" style="position: relative; transform: none; margin: 0 auto;">
<header class="Modal-header">
<h2 class="Modal-title">Small (360px)</h2>
</header>
<div class="Modal-body">
<p>Compact dialogs</p>
</div>
</div>
<div class="Modal" style="position: relative; transform: none; margin: 0 auto;">
<header class="Modal-header">
<h2 class="Modal-title">Medium (480px) — Default</h2>
</header>
<div class="Modal-body">
<p>Standard modal width</p>
</div>
</div>
<div class="Modal Modal--large" style="position: relative; transform: none; margin: 0 auto; max-width: 100%;">
<header class="Modal-header">
<h2 class="Modal-title">Large (720px)</h2>
</header>
<div class="Modal-body">
<p>For complex content or forms</p>
</div>
</div>
</div>
Common Patterns
Confirmation Before Delete
Delete 3 items?
This will permanently remove the selected items. This action cannot be undone.
<div style="position: relative; width: 100%; height: 220px; background: oklch(0% 0 0 / 0.5); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center; padding: var(--space-4);">
<div class="Modal Modal--small" style="position: relative; transform: none;">
<header class="Modal-header">
<h2 class="Modal-title">
<i class="ph ph-warning" style="color: oklch(55% 0.2 25); margin-right: var(--space-2);"></i>
Delete 3 items?
</h2>
</header>
<div class="Modal-body">
<p>This will permanently remove the selected items. This action cannot be undone.</p>
</div>
<footer class="Modal-footer">
<button class="Button Button--secondary">Cancel</button>
<button class="Button Button--danger">Delete Items</button>
</footer>
</div>
</div>
Success Feedback Modal
Payment Successful
Your order has been confirmed and will ship within 2 business days.
<div style="position: relative; width: 100%; height: 220px; background: oklch(0% 0 0 / 0.5); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center; padding: var(--space-4);">
<div class="Modal Modal--small" style="position: relative; transform: none; text-align: center;">
<div class="Modal-body" style="padding-top: var(--space-6);">
<i class="ph ph-check-circle" style="font-size: 3rem; color: oklch(55% 0.15 150); margin-bottom: var(--space-3);"></i>
<h3 style="margin: 0 0 var(--space-2);">Payment Successful</h3>
<p style="color: var(--fg-3);">Your order has been confirmed and will ship within 2 business days.</p>
</div>
<footer class="Modal-footer" style="justify-content: center;">
<button class="Button Button--primary">View Order</button>
</footer>
</div>
</div>
Modal with Scrollable Content
Terms of Service
By using this service you agree to the following terms and conditions. Please read carefully before proceeding.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
<div style="position: relative; width: 100%; height: 280px; background: oklch(0% 0 0 / 0.5); border-radius: var(--r-m); display: flex; align-items: center; justify-content: center; padding: var(--space-4);">
<div class="Modal" style="position: relative; transform: none; max-height: 250px; display: flex; flex-direction: column;">
<header class="Modal-header">
<h2 class="Modal-title">Terms of Service</h2>
<button class="Button Button--icon Button--ghost" aria-label="Close"><i class="ph ph-x"></i></button>
</header>
<div class="Modal-body" style="overflow-y: auto; flex: 1; font-size: 0.85rem; color: var(--fg-3);">
<p>By using this service you agree to the following terms and conditions. Please read carefully before proceeding.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</div>
<footer class="Modal-footer">
<button class="Button Button--secondary">Decline</button>
<button class="Button Button--primary">I Agree</button>
</footer>
</div>
</div>
Customization
Override modal styles using CSS custom properties:
/* Custom modal width */
.Modal--custom {
max-width: 600px;
}
/* Custom overlay color */
.Modal-overlay--branded {
background-color: oklch(25% 0.05 250 / 0.8);
}
/* Remove backdrop blur for performance */
.Modal-overlay--no-blur {
backdrop-filter: none;
}
/* Slide-in from side animation */
.Modal-overlay--slideout .Modal {
position: fixed;
right: 0;
top: 0;
bottom: 0;
max-width: 400px;
height: 100%;
border-radius: 0;
transform: translateX(100%);
}
.Modal-overlay--slideout.active .Modal {
transform: translateX(0);
}
/* Centered content variant */
.Modal--centered .Modal-header {
text-align: center;
justify-content: center;
}
.Modal--centered .Modal-body {
text-align: center;
}
.Modal--centered .Modal-footer {
justify-content: center;
}
API Reference
Overlay Classes
| Class | Description |
|---|---|
.Modal-overlay |
Full-screen backdrop with centering |
.Modal-overlay.active |
Shows the modal (visible, interactive) |
.Modal-overlay--active |
Alternative active class |
.Modal-overlay--nested |
Higher z-index for stacked modals |
Modal Classes
| Class | Description |
|---|---|
.Modal |
Modal container (required) |
.Modal--small |
Small modal (360px max-width) |
.Modal--large |
Large modal (720px max-width) |
.Modal--fullscreen |
Full viewport modal |
.Modal--centered |
Center-aligned header and content |
Structure Classes
| Class | Description |
|---|---|
.Modal-header |
Header area with title and close button |
.Modal-title |
Modal heading text |
.Modal-body |
Main content area (scrollable) |
.Modal-body--scrollable |
Explicit scrollable body |
.Modal-footer |
Action buttons area |
CSS Reference
/* Modal Overlay */
.Modal-overlay {
position: fixed;
inset: 0;
background: oklch(0% 0 0 / 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.Modal-overlay.active,
.Modal-overlay--active {
opacity: 1;
visibility: visible;
}
.Modal-overlay--nested {
z-index: 1001;
}
/* Modal Container */
.Modal {
background: var(--bg);
border: 1px solid var(--bd);
border-radius: var(--r-l);
box-shadow: 0 8px 32px oklch(0% 0 0 / 0.15);
max-width: 480px;
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
transform: translateY(-8px);
transition: transform 0.2s;
}
.Modal-overlay.active .Modal {
transform: translateY(0);
}
/* Sizes */
.Modal--small { max-width: 360px; }
.Modal--large { max-width: 720px; }
.Modal--fullscreen {
max-width: 100%;
max-height: 100%;
height: 100vh;
border-radius: 0;
}
/* Header */
.Modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--bd);
}
.Modal-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
/* Body */
.Modal-body {
padding: var(--space-4) var(--space-5);
flex: 1;
overflow-y: auto;
}
.Modal-body p {
margin: 0 0 var(--space-3);
line-height: 1.5;
color: var(--fg-3);
}
.Modal-body p:last-child {
margin-bottom: 0;
}
/* Footer */
.Modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--space-3);
padding: var(--space-4) var(--space-5);
border-top: 1px solid var(--bd);
}
/* Centered variant */
.Modal--centered .Modal-header { text-align: center; justify-content: center; }
.Modal--centered .Modal-body { text-align: center; }
.Modal--centered .Modal-footer { justify-content: center; }
Accessibility
Proper modal accessibility ensures all users can interact with your dialogs, including those using keyboards and screen readers.
Focus Management
<!-- Modal should trap focus when open -->
<div class="Modal-overlay active" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="Modal">
<header class="Modal-header">
<h2 class="Modal-title" id="modal-title">Accessible Modal</h2>
<button class="Button Button--icon Button--ghost" aria-label="Close modal">
<i class="ph ph-x"></i>
</button>
</header>
<div class="Modal-body">
<p>First focusable element receives focus on open.</p>
<input type="text" class="Input" placeholder="Focus starts here">
</div>
<footer class="Modal-footer">
<button class="Button Button--tertiary">Cancel</button>
<button class="Button Button--primary">Submit</button>
</footer>
</div>
</div>
Focus Trap
Keep focus within the modal while open. Tab cycles through focusable elements without escaping to background content.
// Basic focus trap implementation
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
}
Escape Key
Allow users to close the modal by pressing Escape.
function handleEscapeKey(modal, closeCallback) {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.classList.contains('active')) {
closeCallback();
}
});
}
ARIA Attributes
| Attribute | Element | Description |
|---|---|---|
role="dialog" |
.Modal-overlay |
Identifies as a dialog |
aria-modal="true" |
.Modal-overlay |
Indicates modal behavior |
aria-labelledby |
.Modal-overlay |
Points to title ID |
aria-describedby |
.Modal-overlay |
Points to description (optional) |
aria-label |
Close button | Describes the button action |
Screen Reader Announcements
<!-- Use aria-live for dynamic content -->
<div class="Modal-body">
<div aria-live="polite" aria-atomic="true">
<!-- Dynamic messages announced to screen readers -->
<p class="Alert Alert--success">Changes saved successfully!</p>
</div>
</div>
Keyboard Support
| Key | Action |
|---|---|
| Tab | Move focus to next focusable element |
| Shift + Tab | Move focus to previous focusable element |
| Escape | Close the modal |
| Enter | Activate focused button |
Return Focus
When the modal closes, return focus to the element that triggered it.
let triggerElement = null;
function openModal(modal, trigger) {
triggerElement = trigger;
modal.classList.add('active');
modal.querySelector('input, button')?.focus();
}
function closeModal(modal) {
modal.classList.remove('active');
triggerElement?.focus();
triggerElement = null;
}
Best Practices
Do
- ✓ Use clear, specific titles — “Delete Project?” not “Confirm”
- ✓ Keep content concise — Modals interrupt flow, respect users’ time
- ✓ Provide obvious exit paths — Close button, Cancel action, Escape key
- ✓ Trap focus — Keep keyboard navigation within the modal
- ✓ Return focus — On close, focus returns to the trigger element
- ✓ Prevent background scroll — Body shouldn’t scroll when modal is open
- ✓ Match button order to reading flow — Primary action on the right
- ✓ Use danger styling — Red buttons for destructive actions
Don’t
- ✗ Stack too many modals — Two max; consider page navigation instead
- ✗ Use for complex flows — Long forms belong on dedicated pages
- ✗ Auto-open modals on page load — Let users initiate the interaction
- ✗ Hide the close button — Users must always be able to dismiss
- ✗ Put critical content in modals — Important info should be on the page
- ✗ Use vague actions — “OK” and “Cancel” don’t describe what happens
- ✗ Forget mobile — Ensure modals work on small screens
- ✗ Block background interaction permanently — Always provide an exit