Fixing MaterializeCSS 1.x Modals That Never Open: Vanilla JS Initialization
MaterializeCSS 1.x removed jQuery modal plugins. Learn why modals stay hidden after upgrade and how to initialize them with vanilla JS for static and dynamic content.
27 Oct 2026, 11:44 UTC

Why Your MaterializeCSS Modal Stays Hidden After Upgrading
You upgraded to MaterializeCSS 1.x, kept the same modal markup, and clicked the trigger—but nothing appears. The useful takeaway is that 1.x dropped jQuery entirely and initializes components through the vanilla JavaScript M namespace, meaning plain browser APIs without the jQuery library. If your page still runs $('.modal').modal(), that call does nothing in 1.x because the jQuery plugin path no longer exists.
The 0.x to 1.x Initialization Break
In 0.x releases, modals were jQuery plugins. You selected the element with jQuery and called .modal() on it. In 1.x, the framework exposes a global M object and expects either M.AutoInit() for static pages or explicit M.Modal.init(element, options) calls. Loading jQuery alongside 1.x does not bridge the gap; the old plugin method is simply absent.
Markup and Trigger Wiring
A working modal needs three pieces: a trigger, a wrapper, and content. The trigger is an anchor with the class modal-trigger and an href that matches the modal's id. The modal wrapper carries the class modal, and its inner content area uses modal-content. If you need a persistent action bar, add modal-footer or modal-fixed-footer, but note that the fixed footer variant expects a specific child structure; omitting it can break the bottom bar layout. Content taller than the viewport can scroll independently inside the overlay, which helps with long forms but can behave unexpectedly on very small screens.
Static Pages vs. Dynamically Injected HTML
For markup present at page load, M.AutoInit() scans the DOM and initializes every recognized component. The catch is timing: any modal appended after M.AutoInit() runs is invisible to that scan. If you inject modal HTML via fetch, AJAX, or a client-side template, you must initialize it manually.
Here is a worked example for a modal inserted after an API response. Run this in your browser console or inside the callback that inserts the node:
// After inserting the modal node into the DOM
const modalElement = document.getElementById('user-detail-modal');
if (modalElement && !M.Modal.getInstance(modalElement)) {
M.Modal.init(modalElement, {
dismissible: true,
preventScrolling: true,
onOpenEnd: function() { console.log('modal opened'); }
});
}
The guard !M.Modal.getInstance(modalElement) matters. Calling M.Modal.init on an element that already has an instance can throw an error or produce duplicate overlays, leaving multiple backdrops stacked on the screen.
Trade-offs: Convenience vs. Control
M.AutoInit() is convenient but opaque: you cannot pass per-modal options, and you cannot react to lifecycle events without querying the instance later. Explicit initialization gives you access to options such as opacity, inDuration, outDuration, startingTop, endingTop, and callbacks like onOpenStart, onOpenEnd, onCloseStart, and onCloseEnd. Setting preventScrolling to true stops the background page from scrolling while the modal is open, but you must then ensure the modal content itself is scrollable when it exceeds the viewport. The cost of explicit control is boilerplate: you own the timing and the duplicate-instance checks.
Another limitation is framework integration. MaterializeCSS 1.x does not ship React, Vue, or Angular bindings. If you use a component framework, you must create the M.Modal instance when the component mounts and call destroy() when it unmounts. Skipping the destroy step leaks overlay nodes and event listeners.
Practical Verification
Because component behavior can shift across minor releases, verify against your installed version rather than assuming. Check package.json or the CDN URL for a 1.0.0 or later tag. Then open DevTools and run:
document.querySelector('.modal')
Confirm that M.Modal.getInstance(element) returns an object. Trigger the modal and check that the backdrop appears, the dialog centers, Escape closes it when dismissible is true, and focus returns to the trigger afterward. If the global M object is undefined, you are likely still loading a 0.x jQuery build.
Actionable Closing
Treat every modal as either static or dynamic. Static markup can rely on M.AutoInit(), but dynamic markup demands an explicit M.Modal.init call guarded by getInstance. Verify your version, test the open and close cycle manually, and migrate any remaining jQuery plugin calls before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.