Component
Stable Overlay

Modal

Modals are focused overlay dialogs that interrupt the user to complete a task, confirm an action, or display contextual information without navigating away.

Overview

Every modal has three sections: header (title + close button), body (content), and footer (actions). The overlay uses backdrop-filter:blur(4px) and rgba(0,0,0,.5). The modal enters with a scale(0.95)→scale(1) animation over 0.3s. Clicking the overlay closes the modal.

Action order: Cancel/secondary action is always on the left. Primary/confirm action is always on the right. For destructive modals, the primary action is .btn-danger.

Form Modal

Form Modal (most common)

Click buttons above to see modals

Token Reference

PartClassValue
Overlay bg.modal-overlayrgba(0,0,0,.5) + backdrop-filter:blur(4px)
Container.modalbg:#fff, max-width:500px, radius:16px, shadow-modal
Small.modal.modal-smmax-width:380px
Large.modal.modal-lgmax-width:680px
Header.modal-headerpadding:20px 24px, border-bottom:1px solid #E2E8F0
Title.modal-title16px, fw-800, letter-spacing:-.3px
Close btn.modal-close✕ character, hover:bg:#F1F5F9
Body.modal-bodypadding:24px
Footer.modal-footerpadding:16px 24px, border-top, flex end, gap:12px
Enter animationscale(.95)+opacity(0) → scale(1)+opacity(1), 0.3s ease

Accessibility

RequirementImplementation
Focus trapOn open, move focus to first focusable element inside modal. Tab cycles within modal only.
Escape keyAlways closes the modal. Add keydown listener for e.key === 'Escape'.
Body scrollSet document.body.style.overflow = 'hidden' on open, restore on close.
ARIAAdd role="dialog", aria-modal="true", aria-labelledby pointing to modal title.
Return focusOn close, return focus to the trigger button that opened the modal.

AI Implementation Notes

For AI coding assistants: Modal overlay uses display:none by default, changed to display:flex to show. The overlay itself must be the flex container — never the modal container. Clicking outside the modal (on the overlay) should close it: onclick="if(event.target===this)this.style.display='none'". Destructive modals MUST use .btn-danger for confirm, never .btn-primary. Always include a cancel button.

When to Use

Use Modal when
  • Collecting form data without leaving current page
  • Confirming a destructive action
  • Displaying focused information that requires user response
  • Multi-step workflow in a contained context
Don't use Modal when
  • Content is too complex — use a dedicated page
  • Showing non-critical information — use a toast
  • On mobile for long forms — use a drawer or page instead
  • Nesting modals inside modals — avoid modal stacking

Properties

PropertyTypeRequiredDefaultDescription
openbooleanrequiredfalseControls visibility. display:none → display:flex toggle. Never use visibility:hidden.
onClosefunctionrequiredCalled when user clicks overlay, presses Escape, or clicks close button.
sizestringoptional'default'Width variant.
'sm' (380px) | 'default' (500px) | 'lg' (680px)
titlestringrequiredModal title in header. 16px fw-800. Used for aria-labelledby.
footerReactNodeoptionalundefinedFooter content. Always Cancel (left) + Confirm (right). Use .btn-danger for destructive confirms.

Behavior

TriggerBehavior
Opendisplay:none → display:flex. Focus moves to first focusable element. Body scroll locked (overflow:hidden). Animation: scale(.95)+opacity(0) → scale(1)+opacity(1), 0.3s ease.
Click overlayCloses modal. Implemented via if(event.target===this) check. Only fires when clicking the overlay directly, not modal content.
Escape keyAlways closes modal. Add keydown listener for e.key==='Escape'. Required for accessibility.
Close button× character button in header. Calls onClose handler.
Closedisplay:flex → display:none. Focus returns to trigger element. Body scroll restored.

Best Practices

Do
Cancel left, confirm right. Primary for safe actions.
Don't
Wrong order. Danger for non-destructive. Cancel on right.

Design Decisions

Why scale(.95) → scale(1) for the open animation?
The slight scale-up creates a sense of the modal emerging from the page rather than just appearing. It draws the eye to the center of the screen without being jarring. The 0.3s duration is long enough to feel intentional but short enough to feel snappy.
Why backdrop-filter:blur(4px) on the overlay?
The blur communicates depth — the modal is above the page. It softens the background without making it completely invisible, helping users maintain spatial context of where they were.

Changelog

v1.0.0July 2026Added
  • Initial release — 3 size variants (sm, default, lg)
  • backdrop-filter blur overlay with click-outside close
  • Scale animation on open
  • Focus trap and body scroll lock
  • Escape key closes modal

States

Modal Title
Content area
CancelConfirm
Open
Overlay visible, focus trapped, body scroll locked
No overlay
Closed
display:none, focus returns to trigger

Accessibility

RequirementImplementation
role="dialog"Add to .modal element.
aria-modal="true"Add to .modal element. Tells screen readers this is a modal.
aria-labelledbyPoint to the modal title element ID.
Focus trapTab and Shift+Tab cycle only within modal while open.
Escape keyAlways closes modal. Required.
Return focusOn close, return focus to trigger button.

Composition

Allowed
Form fields in modal body
Alert/callout inside modal body for warnings
Cancel + Confirm in footer (Cancel left, Confirm right)
Scrollable modal-body for long content
Forbidden
Modal inside modal (no stacking)
Primary button for destructive actions (use danger)
Modal without close button or Escape handler

Best Practices

Do
Clear hierarchy. Cancel left, confirm right.
Don't
Use a modal for content that requires full-page context or extensive scrolling.
Complex workflows belong on dedicated pages, not modals.

Anatomy

1
Overlay
div.modal-overlay. position:fixed, inset:0, background:rgba(0,0,0,.5), backdrop-filter:blur(4px), display:flex, align-items/justify-content:center, z-index:1000.
2
Container
div.modal. bg:#fff, max-width:500px, border-radius:16px, overflow:hidden. Animation: scale(.95)+opacity(0) → scale(1)+opacity(1).
3
Header
div.modal-header. padding:20px 24px, border-bottom. Title (16px fw-800) left. Close button (×) right.
4
Body
div.modal-body. padding:24px. Contains form fields, content, or callouts.
5
Footer
div.modal-footer. padding:16px 24px, border-top, justify-content:flex-end, gap:12px. Cancel left, confirm right.

Content Rules

Add New User
Action + object. 16px fw-800. Specific.
Form
Too vague — does not describe what is being done.
Save User
Names the action and object
OK / Submit / Yes
Vague — does not describe what is confirmed