Why Your Monaco Editor Renders Blank Inside a Bootstrap Modal (And How to Fix It)
TLDR: Call editor.setValue() inside the shown.bs.modal event, not at modal open time. Monaco needs a visible, laid-out container to calculate its editor dimensions. A hidden modal has zero width.
Repo: tools/table-formatter
The Problem
TAFNE has a multi-cell bulk edit feature. Select 2+ cells, open the Monaco modal, edit the content (newline = row separator, pipe = column separator), apply.
After wiring up the modal, the Monaco editor appeared visually blank. The textarea was there, it accepted input, and the value was processed correctly on submit. But the syntax highlighting, line numbers, and cursor position display were all missing. The editor looked like an empty white box.
What Monaco Does at Mount Time
Monaco Editor uses editor.create(domNode, options) to initialize. Internally, it measures domNode.offsetWidth and domNode.offsetHeight to calculate how many lines fit in the viewport, where the scrollbar should be, and where the cursor sits.
If domNode is inside a display: none container, offsetWidth returns 0 and offsetHeight returns 0. Monaco proceeds with a zero-width layout. It renders nothing visible because there is no visible space to render into.
The modal's CSS display: none is the direct cause.
Why editor.layout() Does Not Fix It Alone
The first fix attempt was calling editor.layout() after the modal opened:
myModal.addEventListener('shown.bs.modal', () => {
editor.layout();
});
editor.layout() recalculates the container dimensions and re-renders. This works correctly. The editor renders on first open.
But the bulk edit feature sets the editor value before showing the modal:
function openBulkEditModal(cells) {
const content = buildMultiCellContent(cells);
editor.setValue(content); // called while modal is hidden
modal.show();
}
setValue triggers an internal layout pass. That layout pass runs against the still-hidden modal (zero width). The value is set but the display position is calculated against empty geometry.
When shown.bs.modal fires and editor.layout() runs, Monaco recalculates dimensions correctly but does not re-run the cursor and viewport positioning from the setValue call. The content is there, the layout is correct, but the viewport is positioned as if the editor has zero scroll height.
The Fix
Defer setValue to inside shown.bs.modal, after editor.layout():
myModal.addEventListener('shown.bs.modal', () => {
editor.layout();
editor.setValue(pendingContent);
editor.setPosition({ lineNumber: 1, column: 1 });
});
function openBulkEditModal(cells) { pendingContent = buildMultiCellContent(cells); modal.show(); }
layout() first, setValue second. By the time setValue runs, Monaco has a real container width. All its internal geometry is correct. The editor renders with full syntax highlighting, correct line count, and cursor at position 1,1.
The Bootstrap Modal Timing Sequence
Understanding why this works requires knowing the Bootstrap modal event sequence:
show.bs.modalfires (modal starts becoming visible, stilldisplay: none)- CSS transition plays (fade in)
shown.bs.modalfires (modal is fully visible, transition complete)
display changed from none to block and the transition complete. Only at step 3 does offsetWidth return a real value.
Any DOM measurements or layout-dependent initialization must happen in shown.bs.modal, not show.bs.modal and not in the caller before modal.show().
The General Rule
Any JavaScript library that measures a DOM container at initialization time (Monaco, CodeMirror, canvas-based charts, virtual scroll lists) must be initialized or re-initialized inside the shown event of whatever container was previously hidden.
Bootstrap modals, tabs, accordions, and drawers all have a shown event. If a visualization or editor looks wrong when embedded in one of these, deferred initialization is the first thing to try.
// Pattern: deferred init for layout-sensitive components
$('#myModal').on('shown.bs.modal', function () {
if (!editorInitialized) {
editor = monaco.editor.create(containerEl, options);
editorInitialized = true;
} else {
editor.layout();
editor.setValue(pendingContent);
}
});
Tradeoffs
pendingContent is a module-level variable. Multiple rapid modal open/close cycles before shown fires could race. In practice this is not an issue for a user-facing edit flow, but the pattern is fragile if the modal is opened programmatically in quick succession.
editor.setValue resets undo history. The undo stack is cleared on every open. If users expect Ctrl+Z to work across modal sessions, consider editor.getModel().pushEditOperations instead of setValue to preserve history.