Modal
Create dynamic overlays for focused content, notifications, and interactive dialogs with Chassis CSS's modal component.
Overview
Modals create a focused overlay on your interface for important content that requires user attention. They're ideal for confirmations, forms, notifications, and any content that needs to temporarily take precedence over the main interface.
Built with accessible design principles including proper ARIA attributes and keyboard navigation, modals offer flexible sizing from small to full-screen, customizable positioning options, and support for interactive components like forms and grids. Each modal consists of:
- Modal Container controls size and position
- Modal Window provides styling and shadow effects
- Modal Header contains title and close button
- Modal Content is the main area for your content
- Modal Footer is optional area for action buttons
Modal title
<div class="modal" tabindex="-1">
<div class="modal-container">
<div class="modal-window">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="close-button" data-cx-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-content">
<p>Modal content goes here.</p>
</div>
<div class="modal-footer">
<button type="button" class="button secondary" data-cx-dismiss="modal">Close</button>
<button type="button" class="button primary">Save changes</button>
</div>
</div>
</div>
</div> In this example, we use <h5> to maintain clean document outline. In real implementations, considering a modal as its own context, you should use <h1> for the modal title and adjust its appearance with utility classes like .fs-5 as needed.
Implementation
Before implementing modals, note these important considerations:
- Modals shift focus from the main interface, removing scroll from the page body
- By default, only one modal is shown at a time. While stacked modals are technically possible, they should be used sparingly as they can create usability issues
- Place modal HTML at the root level when possible to avoid positioning issues with other fixed elements
- Mobile devices require special handling for fixed-position elements
- The HTML
autofocusattribute doesn't work in modals; use JavaScript instead:
// Focus an element when modal is shown
const myModal = document.getElementById('myModal')
const myInput = document.getElementById('myInput')
myModal.addEventListener('shown.cx.modal', () => {
myInput.focus()
})
Chassis CSS respects user accessibility preferences by automatically disabling animations when the prefers-reduced-motion media query is detected. See the reduced motion guidelines in our accessibility documentation for implementation details.
Examples
Explore these practical examples to see Chassis CSS modals in action across a variety of use cases. Each example demonstrates different features and functionality to help you implement modals effectively in your projects.
Live modal demo
This example shows a fully functional modal that appears when triggered by a button.
<!-- Button to trigger the modal -->
<button type="button" class="button primary" data-cx-toggle="modal" data-cx-target="#exampleModal">
Open modal
</button>
<!-- Modal structure -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-container">
<div class="modal-window">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1>
<button type="button" class="close-button" data-cx-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-content">
<p>You're viewing content in a Chassis CSS modal!</p>
</div>
<div class="modal-footer">
<button type="button" class="button secondary" data-cx-dismiss="modal">Close</button>
<button type="button" class="button primary">Save changes</button>
</div>
</div>
</div>
</div>
Static backdrop
When you need to ensure users interact with your modal before dismissing it, use the static backdrop option. This prevents the modal from closing when clicking outside of it.
<!-- Add data-cx-backdrop="static" and data-cx-keyboard="false" to create a static backdrop modal -->
<div class="modal fade" id="staticBackdrop" data-cx-backdrop="static" data-cx-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
...
</div>
Scrolling modals
Modals can scroll independently from the main page content, which is useful for displaying long forms or information without losing context.
Window scrolling
For long content, the modal scrolls independently from the page. This example has intentionally tall content to demonstrate scrolling behavior.
Content scrolling
For better user experience with long content and fixed headers/footers, use the .modal-container.scrollable class. This keeps the header and footer in view while only scrolling the content area.
<!-- Add .modal-container.scrollable to make only the content area scroll -->
<div class="modal-container scrollable">
...
</div>
Centered modals
Center your modal vertically for better visual balance by adding the .modal-container.centered class.
<!-- Vertically centered modal -->
<div class="modal-container centered">
...
</div>
<!-- Combine centered positioning with scrollable content -->
<div class="modal-container centered scrollable">
...
</div>
Interactive elements
Chassis CSS supports interactive elements like tooltips and popovers within modals. When the modal closes, these elements are automatically dismissed.
<!-- Add data-cx-container to specify the tooltip/popover container -->
<button class="button secondary"
data-cx-toggle="popover"
title="Popover title"
data-cx-content="Popover content"
data-cx-container="#modalId">
Show popover
</button>
<a href="#"
data-cx-toggle="tooltip"
title="Tooltip text"
data-cx-container="#modalId">
Hover for tooltip
</a>
Grid in modals
Create complex layouts within modals using the Chassis CSS grid system.
<div class="modal-content">
<!-- Add container-fluid to use the grid system -->
<div class="container fluid">
<div class="row">
<div class="medium:col-6">Column one</div>
<div class="medium:col-6">Column two</div>
</div>
<!-- Add more rows and columns as needed -->
</div>
</div>
Dynamic content
Use data attributes and JavaScript to dynamically change modal content based on the triggering element.
<button type="button" class="button primary" data-cx-toggle="modal" data-cx-target="#dynamicModal" data-cx-whatever="@first">Content for first user</button>
<button type="button" class="button primary" data-cx-toggle="modal" data-cx-target="#dynamicModal" data-cx-whatever="@second">Content for second user</button>
<button type="button" class="button primary" data-cx-toggle="modal" data-cx-target="#dynamicModal" data-cx-whatever="@chassis">Content for Chassis</button>
<div class="modal fade" id="dynamicModal" tabindex="-1" aria-labelledby="dynamicModalLabel" aria-hidden="true">
<div class="modal-container">
<div class="modal-window">
<div class="modal-header">
<h1 class="modal-title fs-5" id="dynamicModalLabel">New message</h1>
<button type="button" class="close-button" data-cx-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-content">
<form>
<div class="mb-medium">
<label for="recipient-name" class="col-form-label">Recipient:</label>
<input type="text" class="form-input" id="recipient-name" />
</div>
<div class="mb-medium">
<label for="message-text" class="col-form-label">Message:</label>
<textarea class="form-input" id="message-text"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="button secondary" data-cx-dismiss="modal">Close</button>
<button type="button" class="button primary">Send message</button>
</div>
</div>
</div>
</div> // JavaScript to handle dynamic content in the modal
const dynamicModal = document.getElementById('dynamicModal')
if (dynamicModal) {
dynamicModal.addEventListener('show.cx.modal', event => {
// Button that triggered the modal
const button = event.relatedTarget
// Extract data from data-cx-* attributes
const recipient = button.getAttribute('data-cx-whatever')
// Update the modal's content.
const modalTitle = dynamicModal.querySelector('.modal-title')
const recipientInput = dynamicModal.querySelector('#recipient-name')
modalTitle.textContent = `New message to ${recipient}`
recipientInput.value = recipient
})
}
Sequential modals
You can switch between modals by setting up the proper data attributes. This is useful for multi-step forms or wizards.
<button class="button primary" data-cx-target="#firstModal" data-cx-toggle="modal">Open first modal</button>
<!-- First modal -->
<div class="modal fade" id="firstModal" aria-hidden="true" aria-labelledby="firstModalLabel" tabindex="-1">
<div class="modal-container centered">
<div class="modal-window">
<div class="modal-header">
<h1 class="modal-title fs-5" id="firstModalLabel">First step</h1>
<button type="button" class="close-button" data-cx-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-content">
<p>This is the first step of a multi-step process. Continue to the next step with the button below.</p>
</div>
<div class="modal-footer">
<button class="button secondary" data-cx-dismiss="modal">Cancel</button>
<button class="button primary" data-cx-target="#secondModal" data-cx-toggle="modal">Continue to step 2</button>
</div>
</div>
</div>
</div>
<!-- Second modal -->
<div class="modal fade" id="secondModal" aria-hidden="true" aria-labelledby="secondModalLabel" tabindex="-1">
<div class="modal-container centered">
<div class="modal-window">
<div class="modal-header">
<h1 class="modal-title fs-5" id="secondModalLabel">Second step</h1>
<button type="button" class="close-button" data-cx-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-content">
<p>This is the second step. You can go back to the first step or complete the process.</p>
</div>
<div class="modal-footer">
<button class="button default" data-cx-target="#firstModal" data-cx-toggle="modal">Back to step 1</button>
<button class="button secondary" data-cx-dismiss="modal">Cancel</button>
<button class="button primary">Complete</button>
</div>
</div>
</div>
</div> Stacked modals
While generally not recommended for optimal user experience, there are rare cases where you might need to display one modal over another. Here's how to implement this in Chassis CSS:
<button class="button primary" data-cx-target="#baseModal" data-cx-toggle="modal">Open base modal</button>
<!-- Base modal -->
<div class="modal fade" id="baseModal" aria-hidden="true" aria-labelledby="baseModalLabel" tabindex="-1">
<div class="modal-container centered">
<div class="modal-window">
<div class="modal-header">
<h1 class="modal-title" id="baseModalLabel">Base modal</h1>
<button type="button" class="close-button" data-cx-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-content">
<p>This is the base modal. Click the button below to show another modal on top of this one.</p>
<p><strong>Note:</strong> Stacked modals should be used sparingly as they can create confusion and usability issues.</p>
</div>
<div class="modal-footer">
<button class="button secondary" data-cx-dismiss="modal">Close</button>
<button class="button primary" id="showStackedModal">Show stacked modal</button>
</div>
</div>
</div>
</div>
<!-- Stacked modal -->
<div class="modal fade" id="stackedModal" aria-hidden="true" aria-labelledby="stackedModalLabel" tabindex="-1">
<div class="modal-container centered">
<div class="modal-window">
<div class="modal-header">
<h1 class="modal-title" id="stackedModalLabel">Stacked modal</h1>
<button type="button" class="close-button" data-cx-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-content">
<p>This modal is stacked on top of the base modal.</p>
</div>
<div class="modal-footer">
<button class="button primary" data-cx-dismiss="modal">Close and return to base modal</button>
</div>
</div>
</div>
</div> // JavaScript to handle showing a stacked modal
const showStackedModal = document.getElementById('showStackedModal')
if (showStackedModal) {
showStackedModal.addEventListener('click', () => {
chassis.Modal.getOrCreateInstance('#stackedModal').show()
})
}
Customization options
Tailor modals to your specific design and functional requirements with these customization options. Chassis CSS provides a wide range of features for controlling modal appearance, behavior, and responsiveness.
Animation effects
Chassis CSS provides several options for customizing modal animations:
// In your Sass customization
$modal-fade-transform: scale(.9); // Creates a zoom-in effect
$modal-show-transform: none; // End state of the animation
To remove animations entirely, simply omit the .fade class from your modal:
<!-- Modal without fade animation (appears instantly) -->
<div class="modal" tabindex="-1" aria-labelledby="..." aria-hidden="true">
<!-- Modal content -->
</div>
Dynamic height
For modals with changing content height (like expanding accordions), use the handleUpdate() method to recalculate modal position:
// After content changes height
myModal.handleUpdate();
Modal sizes
Chassis CSS offers multiple size options for modals to fit different content needs:
| Size | Class | Max-width |
|---|---|---|
| Small | .modal-small | 300px |
| Default | None | 500px |
| Large | .modal-large | 800px |
| Extra large | .modal-xlarge | 1140px |
<div class="modal-container modal-small">...</div>
<div class="modal-container">...</div> <!-- Default size -->
<div class="modal-container modal-large">...</div>
<div class="modal-container modal-xlarge">...</div>
Fullscreen modals
For immersive experiences or mobile-focused interfaces, use fullscreen modals. You can make them conditionally fullscreen based on viewport size:
| Class | Behavior |
|---|---|
.fullscreen | Always fullscreen |
.small:down:fullscreen | Fullscreen at the small breakpoint and below (< 576px) |
.medium:down:fullscreen | Fullscreen at the medium breakpoint and below (< 768px) |
.large:down:fullscreen | Fullscreen at the large breakpoint and below (< 992px) |
.xlarge:down:fullscreen | Fullscreen at the xlarge breakpoint and below (< 1200px) |
.2xlarge:down:fullscreen | Fullscreen at the 2xlarge breakpoint and below (< 1400px) |
<!-- Always fullscreen -->
<div class="modal-container fullscreen">
<!-- Modal content -->
</div>
<!-- Responsive fullscreen (only on smaller screens) -->
<div class="modal-container medium:down:fullscreen">
<!-- Modal content -->
</div>
Accessibility
Improve modal accessibility with proper ARIA attributes:
<div class="modal"
id="accessibleModal"
tabindex="-1"
aria-labelledby="modalTitle"
aria-describedby="modalDescription"
aria-hidden="true">
<!-- Modal content -->
<h1 id="modalTitle">Modal title</h1>
<p id="modalDescription">This description is announced by screen readers.</p>
</div>
CSS
This component can be customized using CSS variables, allowing for styles to be modified dynamically on the page. These CSS variables are part of Chassis CSS's design token system, giving design teams control over component appearance. See the design tokens page for more details.
Custom properties
These CSS variables control the component's appearance and can be modified dynamically on the page. Components use cascading variables, allowing seamless variations in size, color, and style without redundant style declarations through component inheritance and the context class system.
--#{$prefix}zindex: #{$zindex-modal};
--#{$prefix}width: #{$modal-medium};
--#{$prefix}padding: #{$modal-inner-padding};
--#{$prefix}margin: #{$modal-container-margin};
// Modal window variables
// Color scheme variables
--#{$prefix}fg-color: var(--#{$prefix}modal-fg-color, #{$modal-window-fg-color});
--#{$prefix}bg-color: var(--#{$prefix}modal-bg-color, #{$modal-window-bg-color});
--#{$prefix}border-color: var(--#{$prefix}modal-border-color, #{$modal-window-border-color});
// Border and shape properties
--#{$prefix}border-width: var(--#{$prefix}modal-border-width, #{$modal-window-border-width});
--#{$prefix}border-radius: var(--#{$prefix}modal-border-radius, #{$modal-window-border-radius});
--#{$prefix}inner-border-radius: var(--#{$prefix}modal-inner-border-radius, #{$modal-window-inner-border-radius});
// Header section variables
--#{$prefix}header-padding-y: var(--#{$prefix}modal-header-padding-y, #{$modal-header-padding-y});
--#{$prefix}header-padding-x: var(--#{$prefix}modal-header-padding-x, #{$modal-header-padding-x});
--#{$prefix}header-fg-color: var(--#{$prefix}modal-header-fg-color, #{$modal-header-fg-color});
--#{$prefix}header-bg-color: var(--#{$prefix}modal-header-bg-color, #{$modal-header-bg-color});
--#{$prefix}header-border-color: var(--#{$prefix}modal-header-border-color, #{$modal-header-border-color});
--#{$prefix}header-border-width: var(--#{$prefix}modal-header-border-width, #{$modal-header-border-width});
// Typography settings for modal title
@include map-font($modal-title-font, modal-title, title);
// Footer section variables
--#{$prefix}footer-padding-y: var(--#{$prefix}modal-footer-padding-y, #{$modal-footer-padding-y});
--#{$prefix}footer-padding-x: var(--#{$prefix}modal-footer-padding-x, #{$modal-footer-padding-x});
--#{$prefix}footer-gap: var(--#{$prefix}modal-footer-gap, #{$modal-footer-gap});
--#{$prefix}footer-fg-color: var(--#{$prefix}modal-footer-fg-color, #{$modal-footer-fg-color});
--#{$prefix}footer-bg-color: var(--#{$prefix}modal-footer-bg-color, #{$modal-footer-bg-color});
--#{$prefix}footer-border-color: var(--#{$prefix}modal-footer-border-color, #{$modal-footer-border-color});
--#{$prefix}footer-border-width: var(--#{$prefix}modal-footer-border-width, #{$modal-footer-border-width});
// Shadow variables
--#{$prefix}box-shadow: var(--#{$prefix}modal-box-shadow, #{$modal-window-shadow-xsmall});
--#{$prefix}backdrop-zindex: #{$zindex-modal-backdrop};
--#{$prefix}backdrop-color: #{$modal-backdrop-color};
--#{$prefix}backdrop-opacity: #{$modal-backdrop-opacity};
Sass variables
These Sass variables are also exposed as CSS custom properties using the --cx-
prefix. A Sass variable $variable-name becomes available as --cx-variable-name in CSS, allowing for styles to be modified dynamically on the
page. See the
context components
page for more details.
$modal-small: 18.75rem; //300px;
$modal-medium: 31.25rem; //500px;
$modal-large: 50rem; //800px;
$modal-xlarge: 71.25rem; //1140px;
$modal-fade-transform: scale(1.05);
$modal-show-transform: none;
$modal-transition: transform .3s ease-out;
$modal-scale-transform: scale(1.02);
$modal-backdrop-color: $black;
$modal-backdrop-opacity: .5;
$modal-inner-padding: $spacer;
$modal-container-margin: .5rem;
$modal-container-margin-y-small-up: 2rem;
$modal-title-font: $cx-font-text-xlarge-mass;
$modal-window-padding-y: $spacer;
$modal-window-padding-x: $spacer;
$modal-window-fg-color: null;
$modal-window-bg-color: var(--#{$prefix}bg-main);
$modal-window-border-color: var(--#{$prefix}border-subtle);
$modal-window-border-width: var(--#{$prefix}border-width-medium);
$modal-window-border-radius: var(--#{$prefix}border-radius-xlarge);
$modal-window-inner-border-radius: #{calc($modal-window-border-radius - $modal-window-border-width)};
$modal-window-shadow-xsmall: $cx-shadow-modal-main;
$modal-window-shadow-small-up: $cx-shadow-modal-main;
$modal-header-padding-y: $space-small;
$modal-header-padding-x: $space-medium;
$modal-header-fg-color: null;
$modal-header-bg-color: null;
$modal-header-border-color: var(--#{$prefix}border-subtle);
$modal-header-border-width: $modal-window-border-width;
$modal-footer-padding-y: $space-medium;
$modal-footer-padding-x: $modal-footer-padding-y;
$modal-footer-gap: $space-medium;
$modal-footer-fg-color: null;
$modal-footer-bg-color: null;
$modal-footer-border-color: var(--#{$prefix}border-subtle);
$modal-footer-border-width: $modal-header-border-width;
Sass loops
The responsive fullscreen modal classes are generated automatically using the $breakpoints map:
@each $breakpoint in map-keys($grid-breakpoints) {
$infix: breakpoint-prefix($breakpoint, $grid-breakpoints);
$postfix: if($infix != "", "down\\:", "");
@include media-breakpoint-down($breakpoint) {
&.#{$infix}#{$postfix}fullscreen {
width: 100vw;
max-width: none;
height: 100%;
margin: 0;
.modal-window {
height: 100%;
border: 0;
@include border-radius(0);
}
.modal-header,
.modal-footer {
@include border-radius(0);
}
.modal-content {
overflow-y: auto;
}
}
}
}
JavaScript
Chassis CSS provides a powerful JavaScript API for controlling modals programmatically. You can create, show, hide, and update modals dynamically, allowing for more complex interactions and integration with your application logic.
Data attributes
The simplest way to activate a modal is using data attributes:
<!-- Button to trigger the modal -->
<button data-cx-toggle="modal" data-cx-target="#myModal">Open modal</button>
<!-- Close button within the modal -->
<button data-cx-dismiss="modal">Close</button>
While clicking outside a modal to dismiss it is supported, this approach doesn't follow the ARIA dialog pattern recommendations. For critical content, consider using the static backdrop option.
Initialization
For more control, initialize modals with JavaScript:
// Basic initialization
const myModal = new chassis.Modal(document.getElementById('myModal'))
// With a CSS selector
const anotherModal = new chassis.Modal('#anotherModal')
// With options
const configuredModal = new chassis.Modal('#configuredModal', {
backdrop: 'static',
keyboard: false,
focus: true
})
Configuration options
Chassis components offer flexible configuration through both data attributes and JavaScript. To use data attributes, prefix any option with data-cx- followed by the option name in kebab-case format (using hyphens instead of camelCase). For example, write data-cx-custom-class="my-class" rather than data-cx-customClass="my-class".
For more complex configurations, Chassis provides the data-cx-config attribute which accepts a JSON string of multiple settings. This approach simplifies markup when configuring several options at once. If the same option appears in both data-cx-config and as a separate data attribute (like data-cx-title), the individual attribute takes precedence. You can also use JSON values in individual attributes, such as data-cx-delay='{"show":100,"hide":200}' for more granular control.
When initializing components, Chassis merges configurations from multiple sources in this priority order: default settings, data-cx-config values, individual data-cx-* attributes, and finally any JavaScript object options. Values defined later in this sequence override earlier ones.
| Option | Type | Default | Description |
|---|---|---|---|
backdrop | boolean, 'static' | true | Enables the modal backdrop. Set to 'static' to prevent closing on backdrop click. |
focus | boolean | true | Automatically focuses the modal when initialized. |
keyboard | boolean | true | Enables closing the modal with the escape key. |
Methods
All Chassis CSS component methods are asynchronous and initiate CSS transitions. Methods return immediately when the transition begins, not when it completes. Calling methods on components that are already transitioning will be ignored to prevent conflicts. Learn more about Chassis JavaScript patterns.
| Method | Description |
|---|---|
show() | Displays the modal. Returns before animation completes. |
hide() | Hides the modal. Returns before animation completes. |
toggle() | Toggles the modal visibility. |
handleUpdate() | Manually readjusts modal positioning after content height changes. |
dispose() | Removes the modal functionality and data from the element. |
getInstance() | Static method to get the modal instance associated with an element. |
getOrCreateInstance() | Static method to get existing modal instance or create a new one. |
Usage example:
// Initialize with options
const modal = new chassis.Modal('#myModal', {
keyboard: false
})
// Show the modal programmatically
document.getElementById('showButton').addEventListener('click', () => {
modal.show()
})
// Hide after a user action
document.getElementById('completeAction').addEventListener('click', () => {
// Perform action, then hide modal
saveData().then(() => modal.hide())
})
// Update after content changes
document.getElementById('loadMoreButton').addEventListener('click', () => {
loadMoreContent().then(() => modal.handleUpdate())
})
Events
Chassis CSS provides events to hook into modal behavior:
| Event | Description |
|---|---|
show.cx.modal | Fires immediately when the show method is called. |
shown.cx.modal | Fires when the modal has been made fully visible (after animations). |
hide.cx.modal | Fires immediately when the hide method is called. |
hidden.cx.modal | Fires when the modal is fully hidden (after animations). |
hidePrevented.cx.modal | Fires when modal hiding is prevented (static backdrop click or keyboard:false with escape key). |
Event handling example:
const modalElement = document.getElementById('userFormModal')
// Do something when modal is about to show
modalElement.addEventListener('show.cx.modal', event => {
// Access the button that triggered the modal
const button = event.relatedTarget
// Extract user info from data attributes
const userId = button.getAttribute('data-user-id')
// Update modal content based on user
loadUserData(userId)
})
// Do something when modal is fully shown
modalElement.addEventListener('shown.cx.modal', () => {
// Focus the first input
document.getElementById('userName').focus()
})
// Prevent hiding in certain conditions
modalElement.addEventListener('hide.cx.modal', event => {
const form = modalElement.querySelector('form')
if (form.isDirty() && !confirm('Discard changes?')) {
event.preventDefault()
}
})
// Clean up when modal is hidden
modalElement.addEventListener('hidden.cx.modal', () => {
modalElement.querySelector('form').reset()
})