Skip to content
// Sheets · Apps Script

Insert an image into a cell in Google Sheets.

How to use Apps Script's SpreadsheetApp.newCellImage() builder to place a true in-cell image in Google Sheets, not a floating overlay.

I want to insert an image that lives inside a cell in Google Sheets using Apps Script, not a floating image that slides around when I sort or resize.

The script

copy · paste · trigger
insertCellImage.gs
Apps Script
// Insert a true in-cell image via the CellImage builder.
// sheet.insertImage() floats over the grid — this does not.
function insertCellImage() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var imageUrl = 'https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png';

  var cellImage = SpreadsheetApp.newCellImage()
    .setSourceUrl(imageUrl)
    .setAltTextTitle('Sheets logo')
    .setAltTextDescription('The Google Sheets product icon')
    .build();

  var cell = sheet.getRange('B2');
  cell.setValue(cellImage);
}

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

Walkthrough

Why sheet.insertImage() is the wrong tool

sheet.insertImage(url, col, row) places a Drawing object that floats above the cell grid. It anchors to a position, not a cell. Sort the sheet, insert rows, or auto-resize columns, and the image drifts. That behavior is intentional for charts and annotations, but it is actively harmful when you want an image to travel with its row, the way a product thumbnail should stay next to the SKU it belongs to.

The method you actually want is SpreadsheetApp.newCellImage(), introduced in 2022. It returns a CellImageBuilder, and the result of .build() is a CellImage value that you pass directly to cell.setValue(). The cell treats it like any other value: it moves with the row, copies cleanly, and shows up correctly in sorted or filtered views.

Building and placing the CellImage

The builder chain is short. Call setSourceUrl() with a publicly accessible image URL, optionally call setAltTextTitle() and setAltTextDescription() for accessibility, then call build(). The resulting CellImage object is what goes into setValue().

The URL must be reachable without authentication. A Google Drive link with restricted sharing will silently render as a broken image. I keep a small Cloud Storage bucket (free tier, public-read ACL) for exactly this use case — Drive sharing rules are too fragile to rely on in production pipelines.

After setValue(), the cell's row height may need a nudge if you want the image visible at a reasonable size. setRowHeight(rowIndex, pixels) on the Sheet object handles that. There is no way to set image dimensions inside the CellImage itself; the image scales to fit the cell.

Reading back a CellImage value

cell.getValue() on a cell containing a CellImage returns a CellImage object, not a string or URL. To extract the source URL programmatically, call .getContentUrl() on the returned object. That returns a Google-hosted derivative URL, not the original you passed to setSourceUrl(), so don't rely on it for round-tripping the original URL.

If you are iterating a range and need to test whether a cell holds an image rather than text, check whether the value is an object and whether typeof value === 'object' before calling getContentUrl(). Plain instanceof checks against CellImage are unreliable across script runtimes.

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 newCellImage() work with Google Drive image URLs?
Only if the file is shared publicly (anyone with the link, viewer). A restricted Drive URL will load as a broken image with no error thrown. Use a direct public URL from Cloud Storage or another CDN instead.
Can I resize the image inside the cell?
No. CellImage has no dimension properties. The image scales to fill the cell. To control apparent size, resize the row and column: sheet.setRowHeight(rowIndex, pixels) and sheet.setColumnWidth(colIndex, pixels).
Why does getValue() return an object instead of the image URL?
A cell holding a CellImage stores a structured value, not a string. Call .getContentUrl() on the returned CellImage object to get a URL, but note that Sheets rewrites it to a Google-hosted derivative, not the original source URL you set.
Will in-cell images survive a sort or filter?
Yes. That is the core difference from insertImage(). Because the image is a cell value, it moves with the row during sort, filter, and copy operations, the same as any other cell content.
// one good script a week

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