Engineering Journal
Table Formatter
Table Formatter

Why Your Monaco Editor Renders Blank Inside a Bootstrap Modal (And How to Fix It)

2026-05-22

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:

  1. show.bs.modal fires (modal starts becoming visible, still display: none)
  2. CSS transition plays (fade in)
  3. shown.bs.modal fires (modal is fully visible, transition complete)
Only at step 3 is the modal's 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.

Read this post in the full Engineering Journal →