Skip to content

Styling Modes with CSS

Modes have no built-in look — you define how each mode renders, one CSS file per mode. This is the framework’s styling contract.

Every rendered tile carries the active mode, the block identifier, and one class per element:

<div class="morphic-block morphic-mode-icon-tiles morphic-block-text_print"
style="--morphic-block-color: #5C81A6">
<div class="morphic-element-icon">…</div>
<div class="morphic-element-title">Print</div>
<div class="morphic-element-python">print(<span class="morphic-slot"></span>)</div>
</div>

A mode stylesheet mostly decides which elements are visible and how they’re arranged:

/* icon-tiles.css — show icon + title, hide everything else */
.morphic-mode-icon-tiles .morphic-element-icon { display: block; }
.morphic-mode-icon-tiles .morphic-element-title { display: block; font-weight: 600; }

Views show an element, not a mode, so workspace views and their blocks carry morphic-shows-<element> in place of the mode class (see Presets & Views):

/* python.css: monospace text on workspace blocks showing python */
.morphic-workspace-root.morphic-shows-python .blocklyText {
font-family: "Fira Code", monospace;
}

Mode stylesheets load for every mode, so rules for views can sit in any of them, or in your own CSS.

Blocks are sized for the font your CSS gives their text. The font family, size and weight set on .blocklyText are picked up on every switch, and again once a stylesheet or web font finishes loading, so text never spills out of its block. The same applies to blocks shown on toolbox tiles, which take the font of their element. Other properties, like colors, change nothing about the size.

When your app changes the block font itself, for example with a font size switch, call engine.refresh() afterwards so blocks and tiles are measured again.

Codespaces carry morphic-codespace-root and the element they show, read only ones morphic-read-only too:

.morphic-codespace-root.morphic-shows-python { font-size: 15px; }
.morphic-codespace-root.morphic-read-only { opacity: 0.9; }

Block colors can be driven from CSS via the custom property:

.morphic-block-text_print {
--morphic-block-color: #b469d6;
}

Blockly draws a thin grey line around the workspace. The framework leaves it out; two custom properties bring it back or style it, on the workspace or per mode:

.morphic-workspace-root {
--morphic-workspace-border-color: #c6c6c6;
--morphic-workspace-border-width: 1px;
}

Three ways, in the mount config:

engine.mount({
// 1. A whole folder, by filename (Vite): modes/icon-tiles.css → mode "icon-tiles".
// Links (?url) or CSS text (?raw) both work.
modesFolder: import.meta.glob("./modes/*.css", { eager: true, query: "?url" }),
// 2. Mode by mode, from any bundler or none: CSS text or a link per mode
modeStyles: {
"icon-tiles": "/styles/icon-tiles.css",
python: ".morphic-mode-python { … }",
},
// 3. A base stylesheet applied regardless of mode
baseStyle: { href: "/styles/morphic-base.css" },
});

Each value in modesFolder and modeStyles is either a link or the CSS itself. The framework tells them apart on its own: CSS always contains a {, a link never does. So use whatever your bundler gives you, whether a text import, a URL import, or a file in public/. modesFolder takes precedence over modeStyles for the same mode names.

At mount, the framework checks that every declared mode has a stylesheet and warns about gaps — a mode without CSS renders all elements unstyled, which is almost never what you want. Missing coverage shows up early instead of as a visual surprise later.