cd /news/developer-tools/format-every-table-in-the-active-goo… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-41045] src=gist.github.com β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

Format every table in the active Google Doc

A developer created a Google Apps Script that formats every table in a Google Doc with Arial 8pt font, a blue header row, alternating gray body rows, and zero cell padding. The script also uses the advanced Docs API to set white borders, disable pagination controls, and adjust paragraph spacing outside tables.

read10 min views21 publishedJun 25, 2026

| /** | | | * Format every table in the active Google Doc. | | | * | | | * - Body text: Arial 8 | | | * - Header row: #0b5394 fill, white bold Arial 8 | | | * - Cell padding: 0 on all sides | | | * - Text edges: 0.02" inset on left & right via paragraph indents | | | * (the blue ruler controls), not via cell padding. | | | * | | | * Run formatAllTables(). First run will prompt for authorization. | | | */ | |

| // 0.02 inch, expressed in points (1 in = 72 pt). | |
| var TEXT_INSET_PT = 0.02 * 72; // = 1.44 pt | |
| var FONT_FAMILY = 'Arial'; | |
| var FONT_SIZE = 8; | |
| var HEADER_FILL = '#0b5394'; | |
| var HEADER_TEXT = '#ffffff'; | |
| // Body rows alternate between these two fills (first body row gets [0]). | |
| var ROW_FILLS = ['#f9f9f9', '#efefef']; | |
| function formatAllTables() { | |
| var body = DocumentApp.getActiveDocument().getBody(); | |
| var tables = body.getTables(); | |
| for (var t = 0; t < tables.length; t++) { | |
| formatTable(tables[t], t + 1); | |

| } | | | // Borders are set via the advanced Docs API (DocumentApp can't override | | | // per-cell borders). Requires the "Docs API" advanced service enabled. | | | setWhiteBorders(); | | | // Turn off space-hungry pagination controls across the whole doc. | | | setCompactPagination(); | | | // Body paragraphs (outside tables): 0 before / 4 after. | | | setBodyParagraphSpacing(); | | | } | | | /** | | | * Set spacing to 0pt before / 4pt after on every paragraph OUTSIDE tables. | | | * Top-level paragraph elements are, by definition, not inside table cells, | | | * so iterating them leaves the table spacing rules untouched. | | | * | | | * REQUIRES the "Google Docs API" advanced service. | | | */ | |

| function setBodyParagraphSpacing() { | |
| var docId = DocumentApp.getActiveDocument().getId(); | |
| var doc = Docs.Documents.get(docId); | |
| var content = doc.body.content || []; | |
| var requests = []; | |
| for (var i = 0; i < content.length; i++) { | |
| var el = content[i]; | |

| if (!el.paragraph) continue; // skip tables, section breaks, etc. | |

| requests.push({ | |
| updateParagraphStyle: { | |
| paragraphStyle: { | |
| spaceAbove: { magnitude: 0, unit: 'PT' }, | |
| spaceBelow: { magnitude: 4, unit: 'PT' } | |

| }, | | | fields: 'spaceAbove,spaceBelow', | | | range: { startIndex: el.startIndex, endIndex: el.endIndex } | | | } | | | }); | | | } | |

| if (requests.length > 0) { | |
| Docs.Documents.batchUpdate({ requests: requests }, docId); | |
| Logger.log('Set 0/4 spacing on %s body paragraph(s).', requests.length); | |

| } | | | } | | | /** | | | * Disable the four "space-hungry" pagination settings on EVERY paragraph | | | * in the document (body + tables): Keep with next, Keep lines together, | | | * Prevent single lines (widow/orphan), and Page break before. | | | * | | | * REQUIRES the "Google Docs API" advanced service. | | | */ | |

| function setCompactPagination() { | |
| var docId = DocumentApp.getActiveDocument().getId(); | |
| var doc = Docs.Documents.get(docId); | |
| var content = doc.body.content; | |
| if (!content || content.length === 0) return; | |
| var endIndex = content[content.length - 1].endIndex; | |

| // These three are allowed on paragraphs inside tables, so apply doc-wide. | |

| var requests = [{ | |
| updateParagraphStyle: { | |
| paragraphStyle: { | |

| keepWithNext: false, | | | keepLinesTogether: false, | | | avoidWidowAndOrphan: false | | | }, | | | fields: 'keepWithNext,keepLinesTogether,avoidWidowAndOrphan', | | | range: { startIndex: 1, endIndex: endIndex - 1 } | | | } | | | }]; | | | // pageBreakBefore is rejected on in-table paragraphs, so only target | | | // top-level paragraphs (which are never inside a table). | |

| for (var i = 0; i < content.length; i++) { | |
| var el = content[i]; | |
| if (!el.paragraph) continue; | |
| requests.push({ | |
| updateParagraphStyle: { | |
| paragraphStyle: { pageBreakBefore: false }, | |

| fields: 'pageBreakBefore', | | | range: { startIndex: el.startIndex, endIndex: el.endIndex } | | | } | | | }); | | | } | |

| Docs.Documents.batchUpdate({ requests: requests }, docId); | |
| Logger.log('Disabled keep-with-next / keep-together / widow-orphan doc-wide; page-break-before on body paragraphs.'); | |

| } | |

| function formatTable(table, tableNum) { | |
| var numRows = table.getNumRows(); | |

| // Top-left cell text (row 1, cell 1), for identifying tables in the log. | |

| var firstCellText = '(empty)'; | |
| if (numRows > 0 && table.getRow(0).getNumCells() > 0) { | |
| firstCellText = table.getRow(0).getCell(0).getText(); | |

| } | | | // Figure tables hold images in 1 or 2 cells β€” leave them untouched. | |

| var totalCells = 0; | |
| for (var i = 0; i < numRows; i++) { | |
| totalCells += table.getRow(i).getNumCells(); | |

| } | |

| if (totalCells <= 2) { | |
| Logger.log('Table %s: SKIPPED (%s cells) β€” cell(1,1): "%s"', tableNum, totalCells, firstCellText); | |

| return; | | | } | |

| Logger.log('Table %s: formatting (%s cells) β€” cell(1,1): "%s"', tableNum, totalCells, firstCellText); | |
| for (var r = 0; r < numRows; r++) { | |
| var row = table.getRow(r); | |
| var numCells = row.getNumCells(); | |
| var isHeader = (r === 0); | |

| // Body row index is r-1 so the first body row gets ROW_FILLS[0]. | |

| var bodyFill = isHeader ? null : ROW_FILLS[(r - 1) % ROW_FILLS.length]; | |
| for (var c = 0; c < numCells; c++) { | |
| var cell = row.getCell(c); | |
| formatCell(cell, isHeader, bodyFill); | |

| } | | | } | | | } | | | function formatCell(cell, isHeader, bodyFill) { | | | // Zero cell padding on all sides. | |

| cell.setPaddingTop(0); | |
| cell.setPaddingBottom(0); | |
| cell.setPaddingLeft(0); | |
| cell.setPaddingRight(0); | |

| // (Vertical centering is done via the Docs API in setWhiteBorders β€” | | | // DocumentApp's setVerticalAlignment silently no-ops here.) | | | // Fill: header color, otherwise the alternating body color. | |

| if (isHeader) { | |
| cell.setBackgroundColor(HEADER_FILL); | |
| } else { | |
| cell.setBackgroundColor(bodyFill); | |

| } | | | // Text attributes for everything inside the cell. | |

| var textAttrs = {}; | |
| textAttrs[DocumentApp.Attribute.FONT_FAMILY] = FONT_FAMILY; | |
| textAttrs[DocumentApp.Attribute.FONT_SIZE] = FONT_SIZE; | |
| textAttrs[DocumentApp.Attribute.BOLD] = isHeader; | |
| if (isHeader) { | |
| textAttrs[DocumentApp.Attribute.FOREGROUND_COLOR] = HEADER_TEXT; | |
| } else { | |

| textAttrs[DocumentApp.Attribute.FOREGROUND_COLOR] = null; // leave default | | | } | | | // Paragraph attributes: inset the text edges 0.02" on both sides | | | // using indents (the blue ruler markers), since padding is 0. | |

| var paraAttrs = {}; | |
| paraAttrs[DocumentApp.Attribute.INDENT_START] = TEXT_INSET_PT; | |
| paraAttrs[DocumentApp.Attribute.INDENT_END] = TEXT_INSET_PT; | |
| paraAttrs[DocumentApp.Attribute.INDENT_FIRST_LINE] = TEXT_INSET_PT; | |

| // Headers centered, body left-justified. | | | paraAttrs[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = isHeader | | | ? DocumentApp.HorizontalAlignment.CENTER | | | : DocumentApp.HorizontalAlignment.LEFT; | | | // Single line spacing inside tables. | |

| paraAttrs[DocumentApp.Attribute.LINE_SPACING] = 1; | |
| var numChildren = cell.getNumChildren(); | |
| for (var i = 0; i < numChildren; i++) { | |
| var child = cell.getChild(i); | |
| var type = child.getType(); | |
| if (type === DocumentApp.ElementType.PARAGRAPH) { | |
| var para = child.asParagraph(); | |
| para.setAttributes(paraAttrs); | |

| para.setSpacingAfter(0); // no space after plain paragraphs/line breaks | |

| para.editAsText().setAttributes(textAttrs); | |
| } else if (type === DocumentApp.ElementType.LIST_ITEM) { | |
| var li = child.asListItem(); | |
| li.setAttributes(paraAttrs); | |
| li.setSpacingAfter(2); // 2pt after bullets | |
| li.editAsText().setAttributes(textAttrs); | |
| } else if (type === DocumentApp.ElementType.TABLE) { | |

| // Nested table β€” getTables() doesn't return these, so recurse. | | | formatTable(child.asTable(), 'nested'); | | | } | | | } | | | } | | | /** | | | * Set every cell border to white 0.5pt via the advanced Docs API. | | | * DocumentApp can't reliably override per-cell borders, so we use | | | * updateTableCellStyle, which does. | | | * | | | * REQUIRES: Apps Script editor β†’ Services (+) β†’ add "Google Docs API". | | | */ | |

| function setWhiteBorders() { | |
| var docId = DocumentApp.getActiveDocument().getId(); | |
| var doc = Docs.Documents.get(docId); | |
| var requests = []; | |
| collectBorderRequests(doc.body.content, requests); | |
| if (requests.length > 0) { | |
| Docs.Documents.batchUpdate({ requests: requests }, docId); | |
| Logger.log('Applied white borders to %s table(s).', requests.length); | |
| } else { | |
| Logger.log('No eligible tables found for borders.'); | |

| } | | | } | | | // Walk structural elements, emit one border request per eligible table | |

| // (and recurse into nested tables). Mirrors the <=2-cell skip rule. | |
| function collectBorderRequests(content, requests) { | |
| if (!content) return; | |
| for (var i = 0; i < content.length; i++) { | |
| var el = content[i]; | |
| if (!el.table) continue; | |
| var table = el.table; | |
| var tableRows = table.tableRows || []; | |

| // Count cells; skip figure tables (1 or 2 cells total). | |

| var totalCells = 0; | |
| for (var r = 0; r < tableRows.length; r++) { | |
| totalCells += (tableRows[r].tableCells || []).length; | |

| } | |

| if (totalCells <= 2) continue; | |
| var whiteBorder = { | |
| color: { color: { rgbColor: { red: 1, green: 1, blue: 1 } } }, | |
| width: { magnitude: 0.5, unit: 'PT' }, | |

| dashStyle: 'SOLID' | |

| }; | |
| requests.push({ | |
| updateTableCellStyle: { | |
| tableCellStyle: { | |

| borderTop: whiteBorder, | | | borderBottom: whiteBorder, | | | borderLeft: whiteBorder, | | | borderRight: whiteBorder, | | | contentAlignment: 'MIDDLE' | | | }, | | | fields: 'borderTop,borderBottom,borderLeft,borderRight,contentAlignment', | | | tableStartLocation: { index: el.startIndex } | | | } | | | }); | | | // Recurse into nested tables inside each cell. | |

| for (var rr = 0; rr < tableRows.length; rr++) { | |
| var cells = tableRows[rr].tableCells || []; | |
| for (var cc = 0; cc < cells.length; cc++) { | |
| collectBorderRequests(cells[cc].content, requests); | |

| } | | | } | | | } | | | } |

── more in #developer-tools 4 stories Β· sorted by recency
── more on @google apps script 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/format-every-table-i…] indexed:0 read:10min 2026-06-25 Β· β€”