Skip to main content
dublove
Legend
March 21, 2026
Answered

How-to quickly adjust the bodyRow to align to the textFrame bottom

  • March 21, 2026
  • 3 replies
  • 141 views

Sometimes my table doesn’t quite fill the text box, so I have to manually adjust the row height, which takes several attempts and is quite a hassle.

Is there a way to use a script to adjust the row height of the table body so that the text box is quickly aligned at the bottom?

 

    Correct answer m1b

    Hi ​@dublove, this isn’t as straightforward as we would like.

    I’ve written a script that does it on the test file from your related question and it works, but you will see there are some complications, and the code runs a bit slow because it is testing the cell heights after each scaling test. Still it does the job I think.

    — Mark

     

    Before and after running script
    /**
    * @file Expand Table Rows To Fill Frame.js
    *
    * @author m1b
    * @version 2026-04-11
    * @discussion https://community.adobe.com/questions-671/how-to-quickly-adjust-the-bodyrow-to-align-to-the-textframe-bottom-1554513
    */
    function main() {

    var doc = app.activeDocument;
    var items = doc.selection;

    for (var i = 0; i < items.length; i++)
    expandTableRowsToFillFrame(items[i], true);

    };
    app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Expand Rows To Fill Frame');


    /**
    * Scales the rows of the last table in text frame
    * to their maximum, without overflowing.
    * @author m1b
    * @version 2026-04-11
    * @param {TextFrame} textFrame - the target text frame.
    * @param {Boolean} [onlyBodyRows] - whether to scale only body rows (default: true).
    */
    function expandTableRowsToFillFrame(textFrame, onlyBodyRows) {

    app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS;

    onlyBodyRows = false === onlyBodyRows

    if (TextFrame !== textFrame.constructor)
    return;

    var tf;
    var row;
    var table = null;

    if (
    textFrame.characters.lastItem().isValid
    && '\u0016' === textFrame.characters.lastItem().contents
    )
    table = textFrame.characters.lastItem().tables[0];

    if (!textFrame.characters.lastItem().isValid) {

    // this text frame might have a table, but not the first part of it
    var table = null;
    var timeout = 20;
    var prev = textFrame.previousTextFrame;

    // search for table in previous frames
    while (prev && !table && timeout--) {

    if (
    prev.characters.lastItem().isValid
    && '\u0016' === prev.characters.lastItem().contents
    )
    table = prev.characters.lastItem().tables[0];

    prev = textFrame.previousTextFrame;

    }

    }

    if (!table)
    return;

    var allRows = table.rows;
    var frameRows = [];
    var frameHeights = [];

    for (var i = 0; i < allRows.length; i++) {

    row = allRows[i];
    tf = getParentTextFrame(row, false);

    if (!tf)
    // row is likely overflowing
    break;

    if (tf === textFrame) {
    // found one!
    frameRows.push(row);
    frameHeights.push(row.height);
    }

    else if (frameRows.length)
    // no need to keep looking
    break;

    }

    // now we have the rows we want to target
    var lastRow = frameRows[frameRows.length - 1];

    // find an upper bound where rows overflow the frame
    var low = 1;
    var high = 2;

    while (scaleRows(high))
    high *= 2;

    // binary search for the largest scaleFactor that keeps rows in-frame
    var iterations = 50;
    var precision = 0.001;
    while (high - low > precision && iterations--) {

    var mid = (low + high) / 2;
    if (scaleRows(mid))
    low = mid;
    else
    high = mid;
    }

    // apply the best scale
    scaleRows(low);

    /**
    * Scales original height rows by `scaleFactor`, and returns true, when the frameRows still fit in frame.
    * @param {Number} scaleFactor - multiply the original heights by this.
    * @returns {Boolean}
    */
    function scaleRows(scaleFactor) {

    for (var i = 0; i < frameRows.length; i++)
    if (RowTypes.BODY_ROW === frameRows[i].rowType)
    frameRows[i].height = frameHeights[i] * scaleFactor;

    // if it fits, return true
    return getParentTextFrame(lastRow, false) === textFrame;

    };

    };

    /**
    * Return a row's parent text frame.
    *
    * Notes:
    *
    * (1) Getting the parent text frame of an overset cell is a
    * challenge because there are no insertion points. Here I
    * add a zero-width space as the first character so at least
    * the first insertionPoint will now reside in the text frame.
    *
    * (2) Another complication is a bug where indesign will
    * display the incorrect height for an autogrowing, overflowing
    * cell, and place the cell in the wrong parent text frame.
    * (See explanation here: https://community.adobe.com/t5/indesign-discussions/how-can-get-parenttextframe-of-a-cell-was-overflow/m-p/14030313
    *
    * (3) If your use case is such that returning `undefined` when the
    * cell|row is overset is acceptable, then pass `false` to the
    * `evenWhenCellIsOverset` parameter. This will be much faster.
    *
    * @author m1b
    * @version 2025-04-05
    *
    * @param {Cell|Row} row - an Indesign Table Row or Cell.
    * @param {Boolean} [evenWhenCellIsOverset] - whether to find the parent text frame of an overset cell (default: true).
    * @returns {TextFrame}
    */
    function getParentTextFrame(row, evenWhenCellIsOverset) {

    if (!row.isValid)
    return;

    if (
    false === evenWhenCellIsOverset
    && row.hasOwnProperty('texts')
    )
    // the quick way; will return `undefined` if the cell is overset
    return row.texts[0].parentTextFrames[0];

    if (row.constructor.name == 'Cell')
    row = row.rows[0];

    var cells = row.cells,
    rowTextFrameIDs = {},
    rowTextFrame,
    parentTextFrames = row;

    // find the text frames of the row
    while (
    !parentTextFrames.hasOwnProperty('parentStory')
    || undefined == parentTextFrames.parent
    )
    parentTextFrames = parentTextFrames.parent;

    if (!parentTextFrames.hasOwnProperty('parentStory'))
    return;

    parentTextFrames = parentTextFrames.parentStory.textContainers;

    for (var i = 0; i < cells.length; i++) {

    var cell = cells[i];

    // insert zero-width space to expose first
    // insertion point in case where cell is overset
    cell.insertionPoints[0].contents = '\u200B';

    if (cell.insertionPoints[0].parentTextFrames.length > 0)
    // store the parent frame index of this cell
    rowTextFrameIDs[cell.insertionPoints[0].parentTextFrames[0].id] = true;

    // clean up
    cell.characters[0].remove();

    }

    // match it to the *last-most* parentTextFrame to ensure
    // that the entire row returns the same (correct) text frame
    parentFrameLoop:
    for (var j = parentTextFrames.length - 1; j >= 0; j--) {
    if (rowTextFrameIDs[parentTextFrames[j].id] == true) {
    rowTextFrame = parentTextFrames[j];
    break parentFrameLoop;
    }
    }

    if (
    rowTextFrame != undefined
    && rowTextFrame.isValid
    )
    return rowTextFrame;

    };

     

    3 replies

    m1b
    Community Expert
    m1bCommunity ExpertCorrect answer
    Community Expert
    April 11, 2026

    Hi ​@dublove, this isn’t as straightforward as we would like.

    I’ve written a script that does it on the test file from your related question and it works, but you will see there are some complications, and the code runs a bit slow because it is testing the cell heights after each scaling test. Still it does the job I think.

    — Mark

     

    Before and after running script
    /**
    * @file Expand Table Rows To Fill Frame.js
    *
    * @author m1b
    * @version 2026-04-11
    * @discussion https://community.adobe.com/questions-671/how-to-quickly-adjust-the-bodyrow-to-align-to-the-textframe-bottom-1554513
    */
    function main() {

    var doc = app.activeDocument;
    var items = doc.selection;

    for (var i = 0; i < items.length; i++)
    expandTableRowsToFillFrame(items[i], true);

    };
    app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Expand Rows To Fill Frame');


    /**
    * Scales the rows of the last table in text frame
    * to their maximum, without overflowing.
    * @author m1b
    * @version 2026-04-11
    * @param {TextFrame} textFrame - the target text frame.
    * @param {Boolean} [onlyBodyRows] - whether to scale only body rows (default: true).
    */
    function expandTableRowsToFillFrame(textFrame, onlyBodyRows) {

    app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS;

    onlyBodyRows = false === onlyBodyRows

    if (TextFrame !== textFrame.constructor)
    return;

    var tf;
    var row;
    var table = null;

    if (
    textFrame.characters.lastItem().isValid
    && '\u0016' === textFrame.characters.lastItem().contents
    )
    table = textFrame.characters.lastItem().tables[0];

    if (!textFrame.characters.lastItem().isValid) {

    // this text frame might have a table, but not the first part of it
    var table = null;
    var timeout = 20;
    var prev = textFrame.previousTextFrame;

    // search for table in previous frames
    while (prev && !table && timeout--) {

    if (
    prev.characters.lastItem().isValid
    && '\u0016' === prev.characters.lastItem().contents
    )
    table = prev.characters.lastItem().tables[0];

    prev = textFrame.previousTextFrame;

    }

    }

    if (!table)
    return;

    var allRows = table.rows;
    var frameRows = [];
    var frameHeights = [];

    for (var i = 0; i < allRows.length; i++) {

    row = allRows[i];
    tf = getParentTextFrame(row, false);

    if (!tf)
    // row is likely overflowing
    break;

    if (tf === textFrame) {
    // found one!
    frameRows.push(row);
    frameHeights.push(row.height);
    }

    else if (frameRows.length)
    // no need to keep looking
    break;

    }

    // now we have the rows we want to target
    var lastRow = frameRows[frameRows.length - 1];

    // find an upper bound where rows overflow the frame
    var low = 1;
    var high = 2;

    while (scaleRows(high))
    high *= 2;

    // binary search for the largest scaleFactor that keeps rows in-frame
    var iterations = 50;
    var precision = 0.001;
    while (high - low > precision && iterations--) {

    var mid = (low + high) / 2;
    if (scaleRows(mid))
    low = mid;
    else
    high = mid;
    }

    // apply the best scale
    scaleRows(low);

    /**
    * Scales original height rows by `scaleFactor`, and returns true, when the frameRows still fit in frame.
    * @param {Number} scaleFactor - multiply the original heights by this.
    * @returns {Boolean}
    */
    function scaleRows(scaleFactor) {

    for (var i = 0; i < frameRows.length; i++)
    if (RowTypes.BODY_ROW === frameRows[i].rowType)
    frameRows[i].height = frameHeights[i] * scaleFactor;

    // if it fits, return true
    return getParentTextFrame(lastRow, false) === textFrame;

    };

    };

    /**
    * Return a row's parent text frame.
    *
    * Notes:
    *
    * (1) Getting the parent text frame of an overset cell is a
    * challenge because there are no insertion points. Here I
    * add a zero-width space as the first character so at least
    * the first insertionPoint will now reside in the text frame.
    *
    * (2) Another complication is a bug where indesign will
    * display the incorrect height for an autogrowing, overflowing
    * cell, and place the cell in the wrong parent text frame.
    * (See explanation here: https://community.adobe.com/t5/indesign-discussions/how-can-get-parenttextframe-of-a-cell-was-overflow/m-p/14030313
    *
    * (3) If your use case is such that returning `undefined` when the
    * cell|row is overset is acceptable, then pass `false` to the
    * `evenWhenCellIsOverset` parameter. This will be much faster.
    *
    * @author m1b
    * @version 2025-04-05
    *
    * @param {Cell|Row} row - an Indesign Table Row or Cell.
    * @param {Boolean} [evenWhenCellIsOverset] - whether to find the parent text frame of an overset cell (default: true).
    * @returns {TextFrame}
    */
    function getParentTextFrame(row, evenWhenCellIsOverset) {

    if (!row.isValid)
    return;

    if (
    false === evenWhenCellIsOverset
    && row.hasOwnProperty('texts')
    )
    // the quick way; will return `undefined` if the cell is overset
    return row.texts[0].parentTextFrames[0];

    if (row.constructor.name == 'Cell')
    row = row.rows[0];

    var cells = row.cells,
    rowTextFrameIDs = {},
    rowTextFrame,
    parentTextFrames = row;

    // find the text frames of the row
    while (
    !parentTextFrames.hasOwnProperty('parentStory')
    || undefined == parentTextFrames.parent
    )
    parentTextFrames = parentTextFrames.parent;

    if (!parentTextFrames.hasOwnProperty('parentStory'))
    return;

    parentTextFrames = parentTextFrames.parentStory.textContainers;

    for (var i = 0; i < cells.length; i++) {

    var cell = cells[i];

    // insert zero-width space to expose first
    // insertion point in case where cell is overset
    cell.insertionPoints[0].contents = '\u200B';

    if (cell.insertionPoints[0].parentTextFrames.length > 0)
    // store the parent frame index of this cell
    rowTextFrameIDs[cell.insertionPoints[0].parentTextFrames[0].id] = true;

    // clean up
    cell.characters[0].remove();

    }

    // match it to the *last-most* parentTextFrame to ensure
    // that the entire row returns the same (correct) text frame
    parentFrameLoop:
    for (var j = parentTextFrames.length - 1; j >= 0; j--) {
    if (rowTextFrameIDs[parentTextFrames[j].id] == true) {
    rowTextFrame = parentTextFrames[j];
    break parentFrameLoop;
    }
    }

    if (
    rowTextFrame != undefined
    && rowTextFrame.isValid
    )
    return rowTextFrame;

    };

     

    dublove
    dubloveAuthor
    Legend
    April 11, 2026

    @m1b 

    It is a bit complicated and a bit slow, or perhaps the approach could be optimized.
    Here’s how I’m currently handling it:
    I use two scripts and set two increments:
    add1 = 0.1 mm
    add2 = 0.2 mm
    I run them as needed.
    newHeight = old + add1
    newHeight = old + add2

    It’s also possible to target selected rows

    rob day
    Community Expert
    Community Expert
    March 22, 2026

    Hi ​@dublove , Do you really want the rows of your tables to have inconsistent heights?

     

    You could adjust the Space Above and/or Below of your Table Name paragraph, which would probably be easier to script via a while statement.

     

    Here I’ve removed your empty paragraph returns:

     

     

    dublove
    dubloveAuthor
    Legend
    March 22, 2026

    Hi rob day.

    If you delete the blank lines and adjust the top and bottom margins of the table, there will be noticeable gaps, which won’t look very good.

    I just want to adjust the line height to reduce the bottom spacing (bottomSpace).

    Community Expert
    March 22, 2026

    I’d imagine something like this 

    Get the height of the text frame
    You need the total height available for the table.
    Get the height of the table content
    Measure the current table height (sum of all rows).
    Calculate the difference
    extraSpace = textFrame.height - table.height
    Distribute the extra space to the rows
    Usually added to the last row (or proportionally across all rows).
    Set the new row height
    Adjust the .height property of the row(s) to fill the frame.

    var doc = app.activeDocument;
    var tf = app.selection[0]; // select your text frame first

    if (tf.constructor.name == "TextFrame") {
    var table = tf.tables[0]; // assumes only one table in the frame
    var tableHeight = table.height;
    var frameHeight = tf.geometricBounds[2] - tf.geometricBounds[0]; // bottom - top

    var extraSpace = frameHeight - tableHeight;

    if (extraSpace > 0) {
    // Add extra space to last row
    var lastRow = table.rows[table.rows.length - 1];
    lastRow.height = lastRow.height + extraSpace;
    }

    alert("Table aligned to bottom!");
    } else {
    alert("Please select a text frame with a table first.");
    }

     

    dublove
    dubloveAuthor
    Legend
    March 22, 2026

    @Eugene Tyson 

    That idea might not work out.

    This might require working through the document page by page.

    How do I determine the following values:
    1. The height of the original selection (h1).
    2. The height of the potential target (h2).
    3. The number of rows (rowSel).

     

    I need to repeatedly adjust the minimum row height: h = h2 / rowSel.

    We can assume that a 0.5mm gap between the bottom border of the table on this page and the text box is the desired result.

    Until BotSp = 0.5mm

     

     

    Community Expert
    March 22, 2026

    Thanks for the additional info
     

    You don’t need to iteratively adjust the row height. You can calculate it in one pass:

    Get frame height (h2)
    Subtract your desired gap (e.g. 0.5mm)
    Divide by number of rows

    Then set all row heights to that value.

    Alternatively (better visually), scale existing row heights proportionally:

    scale = targetHeight / table.height

    and multiply each row height by that avoids uneven-looking tables.

    Option 1 — Even row heights (simple + predictable) Fills the frame by making all rows the same height:

    var tf = app.selection[0];

    if (tf.constructor.name !== "TextFrame") {
    alert("Select a text frame first");
    exit();
    }

    if (tf.tables.length === 0) {
    alert("No table found");
    exit();
    }

    var table = tf.tables[0];

    // Frame height
    var frameHeight = tf.geometricBounds[2] - tf.geometricBounds[0];

    // 0.5mm gap (converted to points)
    var gap = 0.5 * 2.83465;

    // Target table height
    var targetHeight = frameHeight - gap;

    // Row count
    var rowCount = table.rows.length;

    // New row height
    var newHeight = targetHeight / rowCount;

    // Apply
    for (var i = 0; i < rowCount; i++) {
    table.rows[i].height = newHeight;
    }


    Option 2 — Proportional scaling (recommended) Keeps the current row proportions and just scales everything to fit:
     

    var tf = app.selection[0];

    if (tf.constructor.name !== "TextFrame") {
    alert("Select a text frame first");
    exit();
    }

    if (tf.tables.length === 0) {
    alert("No table found");
    exit();
    }

    var table = tf.tables[0];

    // Frame height
    var frameHeight = tf.geometricBounds[2] - tf.geometricBounds[0];

    // 0.5mm gap (points)
    var gap = 0.5 * 2.83465;

    // Target height
    var targetHeight = frameHeight - gap;

    // Current table height
    var currentHeight = table.height;

    // Scale factor
    var scale = targetHeight / currentHeight;

    // Apply scaling to each row
    for (var i = 0; i < table.rows.length; i++) {
    table.rows[i].height = table.rows[i].height * scale;
    }