// Sheets · Apps Script

Freeze the header row in Google Sheets.

setFrozenRows(1) locks the header in place as you scroll, but it does nothing to the content or style. Learn the three separate calls — freeze, bold, protect — and when each one matters.

I want to freeze the first row of my spreadsheet so the column headers stay visible when I scroll down, and ideally make it obvious that row is the header.

The script

copy · paste · trigger
freezeHeader.gs
Apps Script
// freezeHeader.gs
// Freezes row 1, bolds the header text, and write-protects that row.
// Run once manually, or wire to onOpen if you want it enforced on every open.

function setupHeader() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var lastCol = sheet.getLastColumn();

  // 1. Freeze the view so row 1 stays visible while scrolling.
  sheet.setFrozenRows(1);

  // 2. Bold the header range — freeze does not do this automatically.
  var headerRange = sheet.getRange(1, 1, 1, lastCol);
  headerRange.setFontWeight('bold');

  // 3. Protect the header row so collaborators cannot edit it.
  var protection = headerRange.protect();
  protection.setDescription('Header row — do not edit');
  protection.setWarningOnly(true);
}

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

Walkthrough

What setFrozenRows actually does (and doesn't do)

sheet.setFrozenRows(1) tells the Sheets rendering engine to pin the first row to the top of the viewport. Scroll down a thousand rows — row 1 stays. That is the entire job of the call. It does not bold anything, does not add a background color, and does not prevent anyone from typing over your headers. The first time I watched a collaborator accidentally clear a frozen header and then panic-undo through the history, I started wiring in the bold and protect steps by default.

The method lives on the Sheet object, not the Range object, which is the first thing to get straight. You call sheet.setFrozenRows(1), not range.setFrozenRows(1). There is no range-level freeze. If you have multiple sheets in the workbook and want each one frozen, you need to call it on each Sheet separately — getSheets() returns an array you can loop over.

Bolding the header range separately

After freezing, getRange(1, 1, 1, lastCol) targets the entire first row out to whatever column the data currently occupies. lastCol comes from sheet.getLastColumn(), which returns the index of the rightmost column that has any content — so the bold range grows automatically if you add columns later.

setFontWeight('bold') is a Range method. The valid string values are 'bold' and 'normal'; anything else is silently ignored by the API, which is exactly the kind of quiet failure that burns time. If you want a background color on the header as well, chain setBackground('#e8eaed') on the same range before the call returns — each formatting method returns the Range, so you can chain them.

Protection: warning-only vs. hard lock

range.protect() returns a Protection object. setWarningOnly(true) means Sheets shows a dialog asking the user to confirm before editing — it does not block the edit. That is usually the right choice for a shared workbook: it stops accidents without blocking someone who genuinely needs to rename a column.

If you want a hard lock where only specific editors can modify the header, drop setWarningOnly(true) and call protection.addEditor('colleague@example.com') instead. Without addEditors, the protection defaults to restricting everyone except the spreadsheet owner. One thing I have watched bite people: if the script is deployed as a Web App running as 'me' (the owner), the protection is set as the owner, which is probably what you want. If it runs as the user triggering it, the protection reflects their permissions instead.

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 setFrozenRows(1) work on every sheet in the workbook, or just the active one?
Just the sheet you call it on. getActiveSheet() targets the tab that's currently selected. To freeze every sheet, iterate: SpreadsheetApp.getActiveSpreadsheet().getSheets().forEach(function(s) { s.setFrozenRows(1); }).
How do I unfreeze the row later?
Call sheet.setFrozenRows(0). Zero rows frozen is the default state. There is no 'unfreeze' method — you just set the count back to zero.
The protection is set but my collaborators can still edit the header. Why?
Check whether setWarningOnly(true) is in your script — warning-only does not block edits, it just prompts. If you need a hard block, remove that line. Also confirm the Protection object was saved: if the script threw an error after protect() but before setWarningOnly(), the protection may have been created without any restriction settings.
Can I freeze multiple rows, not just the header?
Yes. setFrozenRows(n) accepts any positive integer. setFrozenRows(2) freezes rows 1 and 2, which is useful when your data has a title row above the column-label row. The getRange call in the snippet would need to change to getRange(1, 1, 2, lastCol) to cover both rows for bolding and protection.