The issue I’m facing is about vertical alignment inside a GrapesJS component, but the problem can be reproduced even without GrapesJS.
Here is a small, runnable example showing the exact behavior:
<div id="parent" style=" width: 300px; height: 150px; border: 1px solid #ccc; background: #fafafa;"><div id="child" style=" background: #eee; padding: 20px;"> Content</div></div><div style="margin-top: 10px;"><button data-value="flex-start">Top</button><button data-value="center">Middle</button><button data-value="flex-end">Bottom</button></div>document.querySelectorAll("button").forEach((btn) => { btn.addEventListener("click", () => { const parent = document.getElementById("parent"); // In GrapesJS this is parent.addStyle(...) if (parent.style.display !== "flex") { parent.style.display = "flex"; parent.style.flexDirection = "row"; } parent.style.alignItems = btn.dataset.value; });});styleManager.addType("horizontal-align-selector", { create(_args?: any): HTMLElement { const el = document.createElement("div"); el.className = "horizontal-align-selector-container"; el.style.cssText ="display: flex; gap: 8px; padding: 10px 0; flex-wrap: wrap;"; const options: EnhancedRadioOption[] = [ { value: "left", title: "Left", className: "fa fa-align-left" }, { value: "center", title: "Center", className: "fa fa-align-center", }, { value: "right", title: "Right", className: "fa fa-align-right" }, ]; options.forEach((opt) => { const button = document.createElement("button"); button.type = "button"; button.className = "horizontal-align-btn"; button.dataset.value = opt.value; button.title = opt.title || ""; if (opt.className) { const icon = document.createElement("i"); icon.className = opt.className; button.appendChild(icon); } else { button.textContent = opt.title || opt.value; } button.style.cssText = ` min-width: 48px; height: 40px; padding: 10px 12px; border: 2px solid #e5e7eb; background: #ffffff; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; color: #6b7280; border-radius: 7px; `; button.addEventListener("click", () => { const selected = gjsEditor.getSelected(); if (!selected) return; const parent = selected.parent(); if (parent) { const parentStyles = parent.getStyle(); const parentDisplay = parentStyles.display; if (parentDisplay !== "flex" && parentDisplay !== "inline-flex") { parent.addStyle({ display: "flex", }); } let justifyContentValue = "flex-start"; if (opt.value === "center") { justifyContentValue = "center"; } else if (opt.value === "right") { justifyContentValue = "flex-end"; } parent.addStyle({"justify-content": justifyContentValue, }); } updateButtonStates(); }); el.appendChild(button); }); const updateButtonStates = (): void => { const selected = gjsEditor.getSelected(); if (!selected) return; const parent = selected.parent(); if (!parent) return; const parentStyles = parent.getStyle(); const currentValue = parentStyles["justify-content"] || "flex-start"; el.querySelectorAll(".horizontal-align-btn").forEach((button) => { const btnEl = button as HTMLElement; const btnValue = btnEl.dataset.value; // left, center, right let isActive = false; if (btnValue === 'left'&& (currentValue === 'flex-start' || !currentValue)) { isActive = true; } else if (btnValue === 'center'&& currentValue === 'center') { isActive = true; } else if (btnValue === 'right'&& currentValue === 'flex-end') { isActive = true; } if (isActive) { btnEl.style.background = "#6366f1"; btnEl.style.color = "white"; btnEl.style.borderColor = "#6366f1"; btnEl.style.boxShadow = "0 0 0 3px rgba(99, 102, 241, 0.1)"; } else { btnEl.style.background = "white"; btnEl.style.color = "#6b7280"; btnEl.style.borderColor = "#e5e7eb"; btnEl.style.boxShadow = "none"; } }); }; setTimeout(updateButtonStates, 0); gjsEditor.on("component:selected", updateButtonStates); gjsEditor.on("styleable:change", updateButtonStates); return el; }, }); // Vertical Align Selector Type styleManager.addType("vertical-align-selector", { create(_args?: any): HTMLElement { const el = document.createElement("div"); el.className = "vertical-align-selector-container"; el.style.cssText ="display: flex; gap: 8px; padding: 10px 0; flex-wrap: wrap;"; const options: EnhancedRadioOption[] = [ { value: "flex-start", title: "Top", className: "fa fa-arrow-up" }, { value: "center", title: "Middle", className: "fa fa-arrows-v" }, { value: "flex-end", title: "Bottom", className: "fa fa-arrow-down" }, ]; options.forEach((opt) => { const button = document.createElement("button"); button.type = "button"; button.className = "vertical-align-btn"; button.dataset.value = opt.value; button.title = opt.title || ""; if (opt.className) { const icon = document.createElement("i"); icon.className = opt.className; button.appendChild(icon); } else { button.textContent = opt.title || opt.value; } button.style.cssText = ` min-width: 48px; height: 40px; padding: 10px 12px; border: 2px solid #e5e7eb; background: #ffffff; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; color: #6b7280; border-radius: 7px; `; button.addEventListener("click", () => { const selected = gjsEditor.getSelected(); if (!selected) return; const parent = selected.parent(); if (parent) { const parentStyles = parent.getStyle(); const parentDisplay = parentStyles.display; if (parentDisplay !== "flex" && parentDisplay !== "inline-flex") { parent.addStyle({ display: "flex","flex-direction": "row", }); } parent.addStyle({"align-items": opt.value, }); } updateButtonStates(); }); el.appendChild(button); }); const updateButtonStates = (): void => { const selected = gjsEditor.getSelected(); if (!selected) return; const parent = selected.parent(); if(!parent) return; const styles = parent.getStyle(); const currentValue = styles["align-items"] || "flex-start"; el.querySelectorAll(".vertical-align-btn").forEach((button) => { const btnValue = (button as HTMLElement).dataset.value; const isActive = currentValue === btnValue || (btnValue === "flex-start" && (!currentValue || currentValue === "normal" || currentValue === "stretch")); const btnEl = button as HTMLElement; if (isActive) { btnEl.style.background = "#6366f1"; btnEl.style.color = "white"; btnEl.style.borderColor = "#6366f1"; btnEl.style.boxShadow = "0 0 0 3px rgba(99, 102, 241, 0.1)"; } else { btnEl.style.background = "white"; btnEl.style.color = "#6b7280"; btnEl.style.borderColor = "#e5e7eb"; btnEl.style.boxShadow = "none"; } }); }; setTimeout(updateButtonStates, 0); gjsEditor.on("component:selected", updateButtonStates); gjsEditor.on("styleable:change", updateButtonStates); return el; }, });In my actual GrapesJS setup, I created a custom Style Manager input type:
styleManager.addType("vertical-align-selector", { ... });The custom control applies:
display: flex;align-items: flex-start | center | flex-end;Right now, vertical alignment only works if the parent becomes a flex container, so I’m forcing:
display: flex;I want to know: