// Sheets · Apps Script

Add a hyperlink to part of a cell's text in Google Sheets.

Use Apps Script's RichTextValueBuilder to link a substring inside a cell — something the HYPERLINK() formula can't do.

I want to make one word or phrase inside a cell clickable without turning the entire cell into a hyperlink.

The script

copy · paste · trigger
linkSubstring.gs
Apps Script
// Link a substring within a cell's text using RichTextValueBuilder
function linkSubstring() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var cell = sheet.getRange('A1');

  var fullText = 'Read the Apps Script docs for details';
  var linkText = 'Apps Script docs';
  var url = 'https://developers.google.com/apps-script';

  var start = fullText.indexOf(linkText);
  var end = start + linkText.length;

  var richText = SpreadsheetApp.newRichTextValue()
    .setText(fullText)
    .setLinkUrl(start, end, url)
    .build();

  cell.setRichTextValue(richText);
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

Why HYPERLINK() won't cut it

The built-in HYPERLINK() formula wraps the entire cell value in a link. You get one URL per cell, and the display text is the whole thing. That works fine for a standalone URL, but falls apart the moment you want prose like "See the release notes for context" with only "release notes" clickable.

The fix lives in the RichTextValue API, which has been available in Apps Script since 2019 but stays buried because most Sheets users never touch the script editor. It lets you attach formatting (bold, italic, link) to character-level ranges inside a single cell, independently of each other.

How the character offsets work

setLinkUrl(start, end, url) takes zero-based character indices. The start index is inclusive, end is exclusive — the same convention JavaScript's String.prototype.slice() uses, which made it click for me the first time I read the docs.

In the example above, the full string is 'Read the Apps Script docs for details'. The substring 'Apps Script docs' starts at index 9 and is 16 characters long, so end is 25. Using indexOf() to compute those numbers at runtime is safer than hardcoding them: if you later change the surrounding text, the offsets update automatically.

One gotcha: setText() must be called before any setLinkUrl() call on the same builder. If you call them in the wrong order you get a 'text has not been set' error that isn't obvious from the message alone.

Linking multiple phrases in one cell

You can chain as many setLinkUrl() calls as you need before calling build(). Each call covers its own non-overlapping range. Overlapping ranges are allowed by the API but the behavior is undefined in practice — whichever call ran last tends to win, but don't rely on it.

If you need to apply this across many rows, pull the existing cell text with cell.getRichTextValue().getText(), compute your offsets, build the new RichTextValue, and write it back. Reading first preserves any bold or italic runs you set through the UI, because the builder only touches the ranges you explicitly configure.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Does this work on a range of cells, or only one cell at a time?
You can use setRichTextValues() (plural) on a range by passing a 2D array of RichTextValue objects — one per cell, matching the range dimensions. Build each RichTextValue separately, then pass them all in one call to avoid hitting the Sheets API quota.
Will the link survive if I copy the cell to another sheet?
Yes. RichTextValue metadata travels with the cell during copy-paste within Sheets and during sheet duplication. It does not survive an export to CSV or XLSX unless the XLSX reader understands rich text, which most do for hyperlinks.
Can I remove just the link from part of the text without clearing the whole cell?
Call setLinkUrl(start, end, null) with null as the URL. That clears the hyperlink for that range while leaving any other formatting and the cell text intact.
My cell already has bold text set from the UI. Will setRichTextValue() wipe it?
Only if you build a new RichTextValue from scratch with newRichTextValue(). To preserve existing formatting, start from cell.getRichTextValue().copy() to get a RichTextValueBuilder seeded with the current state, then apply your link on top.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.