Looking at your problem, you need to dynamically adjust column widths so that the content heights match. This requires measuring the actual rendered text heights and iteratively adjusting the flex-basis values until the heights are balanced. Here's a React solution that accomplishes this:
// Variables globales
let leftFlex = 1;
let rightFlex = 1;
let isBalancing = false;
// Elementos del DOM
const leftColumn = document.getElementById('leftColumn');
const rightColumn = document.getElementById('rightColumn');
const rebalanceBtn = document.getElementById('rebalanceBtn');
const balancingIndicator = document.getElementById('balancingIndicator');
const leftFlexValue = document.getElementById('leftFlexValue');
const rightFlexValue = document.getElementById('rightFlexValue');
// Función para actualizar la visualización de los valores flex
function updateFlexDisplay() {
leftFlexValue.textContent = leftFlex.toFixed(2);
rightFlexValue.textContent = rightFlex.toFixed(2);
}
// Función para aplicar los valores flex a las columnas
function applyFlexValues() {
leftColumn.style.flex = `${leftFlex} 1 0`;
rightColumn.style.flex = `${rightFlex} 1 0`;
updateFlexDisplay();
}
// Función principal para balancear las columnas
function balanceColumns() {
if (!leftColumn || !rightColumn) {
console.error('No se encontraron las columnas');
return;
}
// Mostrar indicador de balanceado
if (!isBalancing) {
isBalancing = true;
balancingIndicator.style.display = 'inline';
rebalanceBtn.disabled = true;
rebalanceBtn.style.opacity = '0.6';
}
// Obtener las alturas actuales del contenido
const leftHeight = leftColumn.scrollHeight;
const rightHeight = rightColumn.scrollHeight;
console.log(`Alturas actuales - Izquierda: ${leftHeight}px, Derecha: ${rightHeight}px`);
// Si las alturas están lo suficientemente cerca (dentro de 10px), terminamos
if (Math.abs(leftHeight - rightHeight) <= 10) {
console.log('Columnas balanceadas exitosamente');
finishBalancing();
return;
}
// Calcular el factor de ajuste
// Si la columna izquierda es más alta, la hacemos más estrecha (aumentamos su flex)
// Si la columna derecha es más alta, la hacemos más estrecha (aumentamos su flex)
if (leftHeight > rightHeight) {
// Columna izquierda es más alta, hacerla más estrecha
leftFlex *= 1.05;
rightFlex *= 0.98;
console.log('Ajustando: columna izquierda más estrecha');
} else {
// Columna derecha es más alta, hacerla más estrecha
rightFlex *= 1.05;
leftFlex *= 0.98;
console.log('Ajustando: columna derecha más estrecha');
}
// Aplicar los nuevos valores flex
applyFlexValues();
// Continuar balanceando después de un breve retraso para permitir el re-renderizado
setTimeout(() => {
balanceColumns();
}, 50);
}
// Función para finalizar el proceso de balanceado
function finishBalancing() {
isBalancing = false;
balancingIndicator.style.display = 'none';
rebalanceBtn.disabled = false;
rebalanceBtn.style.opacity = '1';
console.log(`Balanceado completado. Valores finales - Izquierda: ${leftFlex.toFixed(2)}, Derecha: ${rightFlex.toFixed(2)}`);
}
// Función para resetear y rebalancear
function resetAndBalance() {
if (isBalancing) {
console.log('Ya se está ejecutando el balanceado');
return;
}
console.log('Iniciando rebalanceado...');
leftFlex = 1;
rightFlex = 1;
applyFlexValues();
// Iniciar el balanceado después de un breve retraso
setTimeout(() => {
balanceColumns();
}, 100);
}
// Función para manejar el redimensionamiento de la ventana
function handleResize() {
if (!isBalancing) {
console.log('Ventana redimensionada, rebalanceando...');
setTimeout(resetAndBalance, 200);
}
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM cargado, iniciando balanceado automático...');
// Aplicar valores iniciales
updateFlexDisplay();
// Iniciar balanceado automático después de que todo esté renderizado
setTimeout(() => {
balanceColumns();
}, 200);
});
// Event listener para el botón de rebalancear
rebalanceBtn.addEventListener('click', resetAndBalance);
// Event listener para redimensionamiento de ventana (con debounce)
let resizeTimeout;
window.addEventListener('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(handleResize, 300);
});
// Funciones de utilidad para debugging
window.debugBalance = {
getHeights: () => ({
left: leftColumn.scrollHeight,
right: rightColumn.scrollHeight,
difference: Math.abs(leftColumn.scrollHeight - rightColumn.scrollHeight)
}),
getCurrentFlex: () => ({
left: leftFlex,
right: rightFlex
}),
forceBalance: () => resetAndBalance()
};
// Variables globales
let leftFlex = 1;
let rightFlex = 1;
let isBalancing = false;
// Elementos del DOM
const leftColumn = document.getElementById('leftColumn');
const rightColumn = document.getElementById('rightColumn');
const rebalanceBtn = document.getElementById('rebalanceBtn');
const balancingIndicator = document.getElementById('balancingIndicator');
const leftFlexValue = document.getElementById('leftFlexValue');
const rightFlexValue = document.getElementById('rightFlexValue');
// Función para actualizar la visualización de los valores flex
function updateFlexDisplay() {
leftFlexValue.textContent = leftFlex.toFixed(2);
rightFlexValue.textContent = rightFlex.toFixed(2);
}
// Función para aplicar los valores flex a las columnas
function applyFlexValues() {
leftColumn.style.flex = `${leftFlex} 1 0`;
rightColumn.style.flex = `${rightFlex} 1 0`;
updateFlexDisplay();
}
// Función principal para balancear las columnas
function balanceColumns() {
if (!leftColumn || !rightColumn) {
console.error('No se encontraron las columnas');
return;
}
// Mostrar indicador de balanceado
if (!isBalancing) {
isBalancing = true;
balancingIndicator.style.display = 'inline';
rebalanceBtn.disabled = true;
rebalanceBtn.style.opacity = '0.6';
}
// Obtener las alturas actuales del contenido
const leftHeight = leftColumn.scrollHeight;
const rightHeight = rightColumn.scrollHeight;
console.log(`Alturas actuales - Izquierda: ${leftHeight}px, Derecha: ${rightHeight}px`);
// Si las alturas están lo suficientemente cerca (dentro de 10px), terminamos
if (Math.abs(leftHeight - rightHeight) <= 10) {
console.log('Columnas balanceadas exitosamente');
finishBalancing();
return;
}
// Calcular el factor de ajuste
// Si la columna izquierda es más alta, la hacemos más estrecha (aumentamos su flex)
// Si la columna derecha es más alta, la hacemos más estrecha (aumentamos su flex)
if (leftHeight > rightHeight) {
// Columna izquierda es más alta, hacerla más estrecha
leftFlex *= 1.05;
rightFlex *= 0.98;
console.log('Ajustando: columna izquierda más estrecha');
} else {
// Columna derecha es más alta, hacerla más estrecha
rightFlex *= 1.05;
leftFlex *= 0.98;
console.log('Ajustando: columna derecha más estrecha');
}
// Aplicar los nuevos valores flex
applyFlexValues();
// Continuar balanceando después de un breve retraso para permitir el re-renderizado
setTimeout(() => {
balanceColumns();
}, 50);
}
// Función para finalizar el proceso de balanceado
function finishBalancing() {
isBalancing = false;
balancingIndicator.style.display = 'none';
rebalanceBtn.disabled = false;
rebalanceBtn.style.opacity = '1';
console.log(`Balanceado completado. Valores finales - Izquierda: ${leftFlex.toFixed(2)}, Derecha: ${rightFlex.toFixed(2)}`);
}
// Función para resetear y rebalancear
function resetAndBalance() {
if (isBalancing) {
console.log('Ya se está ejecutando el balanceado');
return;
}
console.log('Iniciando rebalanceado...');
leftFlex = 1;
rightFlex = 1;
applyFlexValues();
// Iniciar el balanceado después de un breve retraso
setTimeout(() => {
balanceColumns();
}, 100);
}
// Función para manejar el redimensionamiento de la ventana
function handleResize() {
if (!isBalancing) {
console.log('Ventana redimensionada, rebalanceando...');
setTimeout(resetAndBalance, 200);
}
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM cargado, iniciando balanceado automático...');
// Aplicar valores iniciales
updateFlexDisplay();
// Iniciar balanceado automático después de que todo esté renderizado
setTimeout(() => {
balanceColumns();
}, 200);
});
// Event listener para el botón de rebalancear
rebalanceBtn.addEventListener('click', resetAndBalance);
// Event listener para redimensionamiento de ventana (con debounce)
let resizeTimeout;
window.addEventListener('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(handleResize, 300);
});
// Funciones de utilidad para debugging
window.debugBalance = {
getHeights: () => ({
left: leftColumn.scrollHeight,
right: rightColumn.scrollHeight,
difference: Math.abs(leftColumn.scrollHeight - rightColumn.scrollHeight)
}),
getCurrentFlex: () => ({
left: leftFlex,
right: rightFlex
}),
forceBalance: () => resetAndBalance()
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Columnas Balanceadas por Altura</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="controls">
<button id="rebalanceBtn" class="rebalance-button">
Rebalancear Columnas
</button>
<span id="balancingIndicator" class="balancing-indicator" style="display: none;">
Balanceando...
</span>
</div>
<div class="container" id="container">
<div class="column" id="leftColumn">
<h3 class="column-title">Latín</h3>
<p>Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur. Hi omnes lingua, institutis, legibus inter se differunt. Gallos ab Aquitanis Garumna flumen, a Belgis Matrona et Sequana dividit. Horum omnium fortissimi sunt Belgae, propterea quod a cultu atque humanitate provinciae longissime absunt, minimeque ad eos mercatores saepe commeant atque ea quae ad effeminandos animos pertinent important, proximique sunt Germanis, qui trans Rhenum incolunt, quibuscum continenter bellum gerunt. Qua de causa Helvetii quoque reliquos Gallos virtute praecedunt, quod fere cotidianis proeliis cum Germanis contendunt, cum aut suis finibus eos prohibent aut ipsi in eorum finibus bellum gerunt. Eorum una, pars, quam Gallos obtinere dictum est, initium capit a flumine Rhodano, continetur Garumna flumine, Oceano, finibus Belgarum, attingit etiam ab Sequanis et Helvetiis flumen Rhenum, vergit ad septentriones. Belgae ab extremis Galliae finibus oriuntur, pertinent ad inferiorem partem fluminis Rheni, spectant in septentrionem et orientem solem. Aquitania a Garumna flumine ad Pyrenaeos montes et eam partem Oceani quae est ad Hispaniam pertinet; spectat inter occasum solis et septentriones.</p>
<p>Apud Helvetios longe nobilissimus fuit et ditissimus Orgetorix. Is M. Messala, et P. M. Pisone consulibus regni cupiditate inductus coniurationem nobilitatis fecit et civitati persuasit ut de finibus suis cum omnibus copiis exirent: perfacile esse, cum virtute omnibus praestarent, totius Galliae imperio potiri. Id hoc facilius iis persuasit, quod undique loci natura Helvetii continentur: una ex parte flumine Rheno latissimo atque altissimo, qui agrum Helvetium a Germanis dividit; altera ex parte monte Iura altissimo, qui est inter Sequanos et Helvetios; tertia lacu Lemanno et flumine Rhodano, qui provinciam nostram ab Helvetiis dividit. His rebus fiebat ut et minus late vagarentur et minus facile finitimis bellum inferre possent; qua ex parte homines bellandi cupidi magno dolore adficiebantur. Pro multitudine autem hominum et pro gloria belli atque fortitudinis angustos se fines habere arbitrabantur, qui in longitudinem milia passuum CCXL, in latitudinem CLXXX patebant.</p>
</div>
<div class="column" id="rightColumn">
<h3 class="column-title">Inglés</h3>
<p>All Gaul is divided into three parts, one of which the Belgae inhabit, the Aquitani another, those who in their own language are called Celts, in our Gauls, the third. All these differ from each other in language, customs and laws. The river Garonne separates the Gauls from the Aquitani; the Marne and the Seine separate them from the Belgae. Of all these, the Belgae are the bravest, because they are furthest from the civilization and refinement of our Province, and merchants least frequently resort to them, and import those things which tend to effeminate the mind; and they are the nearest to the Germans, who dwell beyond the Rhine, with whom they are continually waging war; for which reason the Helvetii also surpass the rest of the Gauls in valor, as they contend with the Germans in almost daily battles, when they either repel them from their own territories, or themselves wage war on their frontiers. One part of these, which it has been said that the Gauls occupy, takes its beginning at the river Rhone; it is bounded by the river Garonne, the ocean, and the territories of the Belgae; it borders, too, on the side of the Sequani and the Helvetii, upon the river Rhine, and stretches toward the north. The Belgae rises from the extreme frontier of Gaul, extend to the lower part of the river Rhine; and look toward the north and the rising sun. Aquitania extends from the river Garonne to the Pyrenaean mountains and to that part of the ocean which is near Spain: it looks between the setting of the sun, and the north star.</p>
<p>Among the Helvetii, Orgetorix was by far the most distinguished and wealthy. He, when Marcus Messala and Marcus Piso were consuls [61 B.C.], incited by lust of sovereignty, formed a conspiracy among the nobility, and persuaded the people to go forth from their territories with all their possessions, saying that it would be very easy, since they excelled all in valor, to acquire the supremacy of the whole of Gaul. To this he the more easily persuaded them, because the Helvetii, are confined on every side by the nature of their situation; on one side by the Rhine, a very broad and deep river, which separates the Helvetian territory from the Germans; on a second side by the Jura, a very high mountain, which is situated between the Sequani and the Helvetii; on a third by the Lake of Geneva, and by the river Rhone, which separates our Province from the Helvetii. From these circumstances it resulted, that they could range less widely, and could less easily make war upon their neighbors; for which reason men fond of war as they were were affected with great regret. They thought, that considering the extent of their population, and their renown for warfare and bravery, they had narrow limits, although they extended in length 240, and in breadth 180 Roman miles.</p>
</div>
</div>
<div class="flex-values" id="flexValues">
Valores flex actuales: Latín <span id="leftFlexValue">1.00</span>, Inglés <span id="rightFlexValue">1.00</span>
</div>
<script src="script.js"></script>
</body>
</html>
This solution works by:
Measuring actual content heights using scrollHeight on the column elements Iteratively adjusting flex values - when one column is taller, its flex value is increased to make it narrower, which forces the text to wrap more and increases its height Recursive balancing that continues until the height difference is within an acceptable threshold (10px) Safety limits with min/max widths to prevent columns from becoming too narrow or wide
The key insight is that by making a column narrower (higher flex value), you force the text to wrap more, increasing its height. The algorithm finds the right balance where both columns have approximately the same content height. You can adapt this for your React app by:
Extracting the balancing logic into a custom hook Adding it to your existing layout components Adjusting the threshold and adjustment factors for your specific content Adding debouncing if you need to handle window resizing
The "Rebalance Columns" button lets you see the algorithm in action, and the flex values are displayed at the bottom so you can see how the columns are being adjusted.