Google Docs Style Resizer AddOn Menu with Apps Script

https://github.com/lsvekis/Google-Docs-Style-Resizer-AddOn-Menu-with-Apps-Script

When a Doc has been edited by many people (or copy/pasted from other sources), font sizes can drift all over the place. This quick Google Apps Script adds a custom UI menu to your Google Doc that opens a sidebar where you can set font sizes for:

  • Title
  • Subtitle
  • Heading 1–6
  • Normal text

…and then apply those sizes across the whole document (including text inside tables and lists).


What you’ll build

A custom menu in your Doc:

Extensions → Style Resizer → Open Style Resizer

From the sidebar, you choose sizes (e.g., Normal = 12, H1 = 20, H2 = 16…) and click Apply.


Step 1: Create the Apps Script project

  1. Open your Google Doc
  2. Go to Extensions → Apps Script
  3. Create two files:
    • Code.gs
    • Sidebar.html

Step 2: Paste the code

Code.gs

/**
 * Adds a custom menu when the document opens.
 */
function onOpen() {
  DocumentApp.getUi()
    .createMenu('Style Resizer')
    .addItem('Open Style Resizer', 'showStyleResizerSidebar')
    .addSeparator()
    .addItem('Quick: Normal text = 12', 'quickSetNormal12')
    .addToUi();
}

/**
 * Opens the sidebar UI.
 */
function showStyleResizerSidebar() {
  const html = HtmlService.createHtmlOutputFromFile('Sidebar')
    .setTitle('Style Resizer');
  DocumentApp.getUi().showSidebar(html);
}

/**
 * Quick helper: sets NORMAL paragraphs to 12.
 */
function quickSetNormal12() {
  applyStyleSizes({
    NORMAL: 12
  }, false); // includeHeadersFooters = false
}

/**
 * Applies sizes across the doc based on paragraph "heading type".
 *
 * @param {Object} sizeMap e.g. {NORMAL:12, HEADING1:20, TITLE:28, SUBTITLE:16}
 * @param {boolean} includeHeadersFooters whether to apply changes to header/footer too
 */
function applyStyleSizes(sizeMap, includeHeadersFooters) {
  const doc = DocumentApp.getActiveDocument();

  // Validate input sizes (basic safety)
  const cleaned = {};
  Object.keys(sizeMap || {}).forEach(k => {
    const n = Number(sizeMap[k]);
    if (Number.isFinite(n) && n >= 6 && n <= 96) cleaned[k] = n;
  });

  // Apply to body
  walkAndResize_(doc.getBody(), cleaned);

  // Optionally apply to header/footer (if they exist)
  if (includeHeadersFooters) {
    const header = doc.getHeader();
    const footer = doc.getFooter();
    if (header) walkAndResize_(header, cleaned);
    if (footer) walkAndResize_(footer, cleaned);
  }
}

/**
 * Recursively walks container elements and applies font size
 * to Paragraph and ListItem text based on their heading.
 */
function walkAndResize_(container, sizeMap) {
  if (!container || !container.getNumChildren) return;

  for (let i = 0; i < container.getNumChildren(); i++) {
    const child = container.getChild(i);
    const t = child.getType();

    // Paragraphs (includes Title/Subtitle/Headings/Normal)
    if (t === DocumentApp.ElementType.PARAGRAPH) {
      resizeParagraph_(child.asParagraph(), sizeMap);
      continue;
    }

    // Lists
    if (t === DocumentApp.ElementType.LIST_ITEM) {
      resizeListItem_(child.asListItem(), sizeMap);
      continue;
    }

    // Tables
    if (t === DocumentApp.ElementType.TABLE) {
      const table = child.asTable();
      for (let r = 0; r < table.getNumRows(); r++) {
        const row = table.getRow(r);
        for (let c = 0; c < row.getNumCells(); c++) {
          walkAndResize_(row.getCell(c), sizeMap);
        }
      }
      continue;
    }

    // Other container-ish elements (like table cells, body, header, footer)
    if (child.getNumChildren) {
      walkAndResize_(child, sizeMap);
    }
  }
}

function resizeParagraph_(paragraph, sizeMap) {
  const heading = paragraph.getHeading(); // DocumentApp.ParagraphHeading
  const key = headingKey_(heading);
  const size = sizeMap[key];

  if (!size) return;

  // editAsText() applies to text in that paragraph
  const text = paragraph.editAsText();
  if (text) text.setFontSize(size);
}

function resizeListItem_(listItem, sizeMap) {
  // ListItems can still have heading set, but usually NORMAL
  const heading = listItem.getHeading();
  const key = headingKey_(heading);
  const size = sizeMap[key] || sizeMap.NORMAL; // fallback to NORMAL for lists

  if (!size) return;

  const text = listItem.editAsText();
  if (text) text.setFontSize(size);
}

/**
 * Converts ParagraphHeading enum to a plain key used by the UI.
 */
function headingKey_(headingEnum) {
  switch (headingEnum) {
    case DocumentApp.ParagraphHeading.TITLE: return 'TITLE';
    case DocumentApp.ParagraphHeading.SUBTITLE: return 'SUBTITLE';
    case DocumentApp.ParagraphHeading.HEADING1: return 'HEADING1';
    case DocumentApp.ParagraphHeading.HEADING2: return 'HEADING2';
    case DocumentApp.ParagraphHeading.HEADING3: return 'HEADING3';
    case DocumentApp.ParagraphHeading.HEADING4: return 'HEADING4';
    case DocumentApp.ParagraphHeading.HEADING5: return 'HEADING5';
    case DocumentApp.ParagraphHeading.HEADING6: return 'HEADING6';
    case DocumentApp.ParagraphHeading.NORMAL:
    default: return 'NORMAL';
  }
}

Sidebar.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      body { font-family: Arial, sans-serif; padding: 12px; }
      .row { display: flex; justify-content: space-between; align-items: center; margin: 8px 0; }
      label { font-size: 13px; }
      input[type="number"] { width: 90px; padding: 6px; }
      .actions { margin-top: 14px; display: flex; gap: 8px; }
      button { padding: 8px 10px; cursor: pointer; }
      .note { font-size: 12px; opacity: 0.85; margin-top: 10px; line-height: 1.35; }
      .status { margin-top: 10px; font-size: 12px; }
    </style>
  </head>
  <body>
    <h3 style="margin: 0 0 8px;">Style Resizer</h3>

    <div class="row"><label>Title</label><input id="TITLE" type="number" min="6" max="96" value="28"></div>
    <div class="row"><label>Subtitle</label><input id="SUBTITLE" type="number" min="6" max="96" value="16"></div>
    <div class="row"><label>Heading 1</label><input id="HEADING1" type="number" min="6" max="96" value="20"></div>
    <div class="row"><label>Heading 2</label><input id="HEADING2" type="number" min="6" max="96" value="16"></div>
    <div class="row"><label>Heading 3</label><input id="HEADING3" type="number" min="6" max="96" value="14"></div>
    <div class="row"><label>Heading 4</label><input id="HEADING4" type="number" min="6" max="96" value="13"></div>
    <div class="row"><label>Heading 5</label><input id="HEADING5" type="number" min="6" max="96" value="12"></div>
    <div class="row"><label>Heading 6</label><input id="HEADING6" type="number" min="6" max="96" value="12"></div>
    <div class="row"><label>Normal text</label><input id="NORMAL" type="number" min="6" max="96" value="12"></div>

    <div class="row" style="margin-top: 12px;">
      <label>
        <input id="includeHF" type="checkbox">
        Include header & footer
      </label>
    </div>

    <div class="actions">
      <button onclick="apply()">Apply</button>
      <button onclick="setDefaults()">Reset defaults</button>
    </div>

    <div class="note">
      Tip: sizes are “Doc font sizes” (e.g., 12 ≈ 12px feel). This updates text inside paragraphs, lists, and tables.
    </div>

    <div id="status" class="status"></div>

    <script>
      function setDefaults() {
        const defaults = {
          TITLE: 28, SUBTITLE: 16,
          HEADING1: 20, HEADING2: 16, HEADING3: 14, HEADING4: 13, HEADING5: 12, HEADING6: 12,
          NORMAL: 12
        };
        Object.keys(defaults).forEach(k => document.getElementById(k).value = defaults[k]);
        setStatus("Defaults restored.");
      }

      function apply() {
        const keys = ["TITLE","SUBTITLE","HEADING1","HEADING2","HEADING3","HEADING4","HEADING5","HEADING6","NORMAL"];
        const map = {};
        keys.forEach(k => map[k] = Number(document.getElementById(k).value));

        const includeHF = document.getElementById("includeHF").checked;

        setStatus("Applying sizes…");
        google.script.run
          .withSuccessHandler(() => setStatus("Done! Styles resized across the document."))
          .withFailureHandler(err => setStatus("Error: " + (err && err.message ? err.message : err)))
          .applyStyleSizes(map, includeHF);
      }

      function setStatus(msg) {
        document.getElementById("status").textContent = msg;
      }
    </script>
  </body>
</html>

How to use it

  1. Reload the Doc (or run onOpen() once from the editor)
  2. In the Doc: Style Resizer → Open Style Resizer
  3. Set sizes you want
  4. Click Apply

Notes and tips

  • “px vs pt?” Google Docs uses font size numbers (commonly thought of as pt). In practice, setting 12 gives you the “12px-ish” standard body feel in Docs.
  • This script resizes the text inside each paragraph/list item, based on its heading type (Normal, Heading 1, etc.).
  • It also handles tables and lists.
  • Optional: apply to header/footer too.

Common customization ideas

If you want this to also enforce:

  • Font family (e.g., Arial everywhere)
  • Line spacing
  • Spacing before/after
  • Heading colors

…tell me what rules you want and I’ll extend the sidebar to include them.