The Apps Script Lab

๐Ÿ•ต๏ธโ€โ™€๏ธ Add Invisible IDs to Google Docs Paragraphs with Apps Script

Ever wanted toย track paragraphsย in a Google Doc without adding visible clutter? Whether you’re syncing content with an external system, managing edits across collaborators, or linking comments to specific sections, this technique lets you embed invisible unique IDs inside your document using zero-width characters. ๐Ÿคฏ

Letโ€™s dive into how this works, and how you can use it in your own Docs.

๐Ÿงฉ What problem does this solve?

Google Docs doesnโ€™t let you attach custom metadata to paragraphs. So if you need to:

โ€ฆyouโ€™re stuck. Or at least, you were. Now, with this trick, weโ€™ll inject an invisible UUID into each paragraph using a binary encoding made of zero-width characters (theyโ€™re 100% invisible to users).

Hereโ€™s the full script that:

๐Ÿ” Encode any string into invisible characters

PrismJS Styled Code Block
JSCode.gs

function encodeToInvisible(input) {
  return input
    .split('')
    .map(char => {
      const binary = char.charCodeAt(0).toString(2).padStart(8, '0');
      return binary.replace(/0/g, '\u200B').replace(/1/g, '\u200C');
    })
    .join('');
}
    

๐Ÿท๏ธ Assign invisible IDs to all paragraphs

Code.gs - Google Apps Script Example
JSCode.gs

function assignInvisibleIdsToParagraphs() {
  const body = DocumentApp.getActiveDocument().getBody();
  const paragraphs = body.getParagraphs();

  paragraphs.forEach((para) => {
    const text = para.getText();
    const clean = text.replace(/[\u200B\u200C]/g, '');
    if (clean.length !== text.length) return; // already has an invisible tag

    const uuid = Utilities.getUuid();
    const hidden = encodeToInvisible(uuid);
    para.insertText(0, hidden);
  });
}
    

๐Ÿงผ Decode the invisible string back to readable text

decodeInvisible() โ€“ Code.gs
JSCode.gs

function decodeInvisible(encoded) {
  const binary = encoded
    .split('')
    .filter(c => c === '\u200B' || c === '\u200C')
    .map(c => c === '\u200B' ? '0' : '1')
    .join('');

  let output = '';
  for (let i = 0; i < binary.length; i += 8) {
    const byte = binary.slice(i, i + 8);
    if (byte.length < 8) break;
    output += String.fromCharCode(parseInt(byte, 2));
  }
  return output;
}
    

๐Ÿ“œ Log all paragraph UUIDs to a Google Sheet

logInvisibleUUIDsInParagraphs()
JSCode.gs

function logInvisibleUUIDsInParagraphs() {
  const ss = SpreadsheetApp.openByUrl(YOUR_SS_URL);
  const sheet = ss.getSheets()[0];

  const body = DocumentApp.getActiveDocument().getBody();
  const paragraphs = body.getParagraphs();
  const rows = [];

  paragraphs.forEach((para, i) => {
    const text = para.getText();
    const hiddenPrefix = text.match(/^[\u200B\u200C]{8,}/);
    if (hiddenPrefix) {
      const decoded = decodeInvisible(hiddenPrefix[0]);
      rows.push([decoded, text]);
    }
  });

  sheet.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
}
    

โœ๏ธ Edit a paragraph but preserve its hidden ID

replaceParagraphTextPreservingInvisibleId()
JSCode.gs

function replaceParagraphTextPreservingInvisibleId(para, newVisibleText) {
  const fullText = para.getText();
  const match = fullText.match(/^([\u200B\u200C]+)/);
  const invisiblePrefix = match ? match[1] : '';
  para.setText(invisiblePrefix + newVisibleText);
}
    

๐Ÿ” Get the invisible ID from the currently selected paragraph

getSelectedParagraphId()
JSCode.gs

function getSelectedParagraphId() {
  const doc = DocumentApp.getActiveDocument();
  const selection = doc.getSelection();
  if (!selection) return null;

  let element = selection.getRangeElements()[0]?.getElement();
  while (element && element.getType() !== DocumentApp.ElementType.PARAGRAPH) {
    element = element.getParent();
  }

  if (!element || element.getType() !== DocumentApp.ElementType.PARAGRAPH) return null;

  const text = element.getText();
  const match = text.match(/^([\u200B\u200C]{64,})/);
  return match ? decodeInvisible(match[1]) : null;
}
    

๐Ÿง  Use Cases

โœ… You’re Ready!

You now have a reliable way to embed invisible, permanent IDs into your paragraphs. Use it to build smarter Docs automations or enhance content traceability, without adding a single visible character. ๐Ÿ‘ป

Let me know if youโ€™d like a follow-up on how to make these IDs clickable, extractable via UI, or synced in real time with Sheets!

โš–๏ธ Comparison: Hidden ID vs Named Range

Google Sheets Embed Replica
Google Sheets Icon Hidden IDs VS Named ranges
Scroll to Top